├── .editorconfig ├── .github └── workflows │ └── dub.yml ├── .gitignore ├── .travis.yml ├── .vscode ├── extensions.json └── tasks.json ├── Brewfile ├── LICENSE ├── README.md ├── dub.json ├── eloquent.sublime-project ├── i18n ├── text.de_DE.po ├── text.en_GB.po ├── text.en_US.po └── text.ru_RU.po ├── package.json ├── public ├── robots.txt └── styles │ └── style.css ├── schema.sql ├── screenshot.png ├── source ├── app.d ├── config │ ├── context.d │ ├── database.d │ ├── logging │ │ ├── colorstdlogger.d │ │ ├── colorvibelogger.d │ │ └── package.d │ ├── motd.d │ └── properties.d ├── controllers │ ├── admincontroller.d │ ├── package.d │ └── webappcontroller.d ├── model │ ├── blogpost.d │ ├── comment.d │ ├── package.d │ └── user.d └── services │ ├── blogservice.d │ ├── package.d │ └── userservice.d ├── views ├── admin_blogposts.dt ├── admin_comments.dt ├── admin_users.dt ├── error.dt ├── header.dt ├── index.dt ├── layout.dt ├── login.dt └── profile.dt └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | 7 | [*.{c,h,d,di,dd,dt}] 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 4 11 | trim_trailing_whitespace = true 12 | 13 | [*.{yml,json}] 14 | indent_size = 2 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/workflows/dub.yml: -------------------------------------------------------------------------------- 1 | name: dub test 2 | 3 | on: 4 | # schedule: 5 | # - cron: '30 7 1 * *' 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | 14 | jobs: 15 | test: 16 | name: ${{ matrix.compiler }} on ${{ matrix.os }} 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | os: [ ubuntu-latest ] # don't bother with windows-latest or macOS-latest 21 | compiler: 22 | - dmd-latest 23 | - ldc-latest 24 | - dmd-2.106.1 # (released in 2024) 25 | - dmd-2.105.3 # (released in 2023) 26 | - dmd-2.104.2 # (released in 2023) 27 | - dmd-2.103.1 # (released in 2023) 28 | - dmd-2.102.2 # (released in 2023) 29 | # - dmd-2.101.2 # (released in 2023) ## excluded due to a compiler bug 30 | - dmd-2.100.2 # (released in 2022) ## GDC 12 can support 2.100 31 | - dmd-2.099.1 # (released in 2022) 32 | # - dmd-2.098.1 # (released in 2021) ## Has issue re: phobos/std/variant.d 33 | - dmd-2.097.2 # (released in 2021) ## HibernateD & DDBC require >=2.097 34 | 35 | - ldc-1.35.0 # eq to dmd v2.105.2 36 | - ldc-1.34.0 # eq to dmd v2.104.2 37 | - ldc-1.33.0 # eq to dmd v2.103.1 38 | - ldc-1.28.1 # eq to dmd v2.098.1 39 | - ldc-1.27.1 # eq to dmd v2.097.2 40 | 41 | include: 42 | - { os: windows-latest, compiler: dmd-latest } # Windows Server 2022 43 | - { os: windows-latest, compiler: ldc-latest } # Windows Server 2022 44 | - { os: macos-latest, compiler: dmd-latest } # macOS 12 45 | - { os: macos-latest, compiler: ldc-latest } # macOS 12 46 | 47 | runs-on: ${{ matrix.os }} 48 | steps: 49 | - uses: actions/checkout@v4 50 | 51 | - name: Install D ${{ matrix.compiler }} 52 | uses: dlang-community/setup-dlang@v1 53 | with: 54 | compiler: ${{ matrix.compiler }} 55 | 56 | - name: Install dependencies on Ubuntu 57 | if: startsWith(matrix.os, 'ubuntu') 58 | run: sudo apt-get update && sudo apt-get install libev-dev libevent-dev libsqlite3-dev -y 59 | 60 | # - name: Install dependencies on Mac OSX 61 | # if: startsWith(matrix.os, 'mac') 62 | # run: brew bundle 63 | 64 | - name: build and run the unittest config 65 | run: dub test 66 | 67 | 68 | # ## On Ubuntu we can use GDC. The compatibility of gdc is: 69 | # ## gcc gdc-10 -> D 2.076 (the default on Ubuntu 20.04 (ubuntu-latest), also available on 22.04) 70 | # ## gcc gdc-11 -> D 2.076 (requires Ubuntu 22.04) 71 | # ## gcc gdc-12 -> D 2.100 (requires Ubuntu 22.04) 72 | # ## gcc gdc-13 -> D 2.??? (requires Ubuntu 23.04 - no runners yet) 73 | # ## Until DDBC can support gdc this cannot be used. See https://github.com/buggins/ddbc/issues/122 74 | # gdc: 75 | # name: ${{ matrix.compiler }} on ${{ matrix.os }} 76 | # strategy: 77 | # fail-fast: false 78 | # matrix: 79 | # os: [ ubuntu-22.04 ] 80 | # compiler: [ gdc-12 ] 81 | # runs-on: ${{ matrix.os }} 82 | # steps: 83 | # - uses: actions/checkout@v4 84 | # - name: Install DMD (so dub is available) 85 | # uses: dlang-community/setup-dlang@v1 86 | # with: 87 | # compiler: dmd-latest 88 | # - name: Install ${{ matrix.compiler }} 89 | # run: | 90 | # sudo apt update 91 | # sudo apt install ${{ matrix.compiler }} -y 92 | # - name: Install system dependencies 93 | # run: sudo apt install libev-dev libevent-dev libsqlite3-dev -y 94 | # - name: Show version 95 | # run: | 96 | # ${{ matrix.compiler }} --version 97 | # dub --version 98 | # - name: Dub Build (Release) 99 | # env: 100 | # DC: ${{ matrix.compiler }} 101 | # run: dub build --compiler=${{ matrix.compiler }} --build=release 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | dub.selections.json 3 | settings.json 4 | docs.json 5 | __dummy.html 6 | *.dll 7 | *.o 8 | *.obj 9 | *.pdb 10 | 11 | __test__library__ 12 | 13 | # Ignore code coverage: 14 | *.lst 15 | .*.lst 16 | 17 | # Ignore client side dependencies 18 | node_modules/ 19 | public/css/*.css 20 | public/js/*.js 21 | public/fonts/ 22 | 23 | # Ignore Intellij IDEA folder: 24 | .idea 25 | *.iml 26 | 27 | # Ignore Sublime Text workspace (project file is ok): 28 | *.sublime-workspace 29 | 30 | # Ignore the executable and log file that is generated when running the app 31 | eloquent 32 | eloquent.exe 33 | *.log 34 | *.sqlite 35 | 36 | # ignore properties that are used when running the application 37 | *.properties 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # travis-ci.org supports D, using dub with various compilers 2 | # For available compilers see: https://semitwist.com/travis-d-compilers 3 | language: d 4 | 5 | d: 6 | - ldc 7 | - dmd 8 | - dmd-2.094.2 9 | - dmd-2.093.1 10 | - dmd-2.092.1 11 | - dmd-2.091.1 12 | - ldc-1.24.0 # eq to dmd v2.094.1 13 | - ldc-1.23.0 # eq to dmd v2.093.1 14 | - ldc-1.22.0 # eq to dmd v2.092.1 15 | - ldc-1.21.0 # eq to dmd v2.091.1 16 | 17 | install: 18 | - npm install -g bower 19 | 20 | script: 21 | - dub test --compiler=${DC} 22 | 23 | addons: 24 | apt: 25 | update: true 26 | sources: 27 | - ubuntu-toolchain-r-test 28 | packages: [ libev-dev, libevent-dev, libsqlite3-dev ] 29 | homebrew: 30 | brewfile: true 31 | 32 | cache: 33 | yarn: true 34 | directories: 35 | - node_modules 36 | 37 | matrix: 38 | include: 39 | - env: NAME="Test Coverage - coveralls.io" 40 | d: dmd 41 | os: osx 42 | osx_image: xcode12.2 # use OSX 10.15.7 43 | before_script: dub fetch doveralls && dub test -b unittest-cov --compiler=${DC} 44 | script: dub run doveralls 45 | - d: dmd 46 | os: osx 47 | osx_image: xcode12.2 # use OSX 10.15.7 48 | - d: ldc 49 | os: osx 50 | osx_image: xcode12.2 # use OSX 10.15.7 51 | - d: dmd-beta 52 | - d: gdc 53 | allow_failures: 54 | - d: dmd-beta 55 | - d: gdc 56 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["webfreak.code-d", "github.vscode-github-actions"] 3 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Dub Build", 8 | "type": "shell", 9 | "command": "dub", 10 | "args": ["build"], 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | }, 15 | "presentation": { 16 | "reveal": "always", 17 | "clear": true, 18 | "panel": "shared" 19 | } 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | # running 'brew bundle' will install required dependencies 2 | brew 'libev' 3 | brew 'libevent' 4 | brew 'sqlite' 5 | brew 'yarn' unless system 'yarn' 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Eloquent 2 | ======== 3 | 4 | ![dub test](https://github.com/SingingBush/eloquent/workflows/dub%20test/badge.svg) 5 | 6 | [![Coverage Status](https://coveralls.io/repos/github/SingingBush/eloquent/badge.svg?branch=master)](https://coveralls.io/github/SingingBush/eloquent?branch=master) 7 | 8 | Eloquent is a lightweight web application written in [D](http://dlang.org). It is essentially an implementation of Wordpress. If you already have a Wordpress installation you can use the existing database and serve the content using Eloquent instead of the usual application written in PHP. 9 | 10 | > This application is currently a work in progress and should only currently be used as an example of how to get going with developing web applications in D. Functionality is currently limited to merely displaying existing blog posts. 11 | 12 | ![Wordpress blog posts being served by Eloquent](screenshot.png) 13 | 14 | ## Main dependencies 15 | 16 | - [Vibe.d](http://vibed.org) Asynchronous I/O web toolkit. 17 | - [Poodinis](https://github.com/mbierlee/poodinis) Dependency Injection Framework. 18 | - [Hibernated](https://github.com/buggins/hibernated) ORM for D. 19 | 20 | ## Yet to implement 21 | 22 | - editing/creating content 23 | - comments 24 | - internationalisation 25 | 26 | ## Running the application 27 | 28 | You'll need an existing wordpress database running on MySQL or Mariadb. The application looks for a file named `app.properties` that should be placed in the same directory as the compiled executable. 29 | 30 | The properties file should contain your database connection details and path for an error log in the following format: 31 | 32 | ```properties 33 | http.port=80 34 | 35 | ## if not specified the db will default to SQLite 36 | db.dialect=MySQL 37 | db.domain=localhost 38 | db.port=3306 39 | db.name=mywordpressdb 40 | db.user=dbusername 41 | db.password=dbPassword 42 | db.createSchema=false 43 | 44 | auth.salt=RANDOMLY GENERATED 32-bit SALT HERE 45 | 46 | # optional log settings. The default log level is info (options: verbose, debug, trace, error, info) with filename as eloquent-server.log 47 | log.file=eloquent-server.log 48 | log.level=trace 49 | ``` 50 | 51 | Or for a more minimal config for local development use SQLite: 52 | 53 | ```properties 54 | db.dialect=SQLite 55 | db.createSchema=true 56 | ``` 57 | 58 | ## Building 59 | 60 | You'll need a D compiler and [DUB](http://code.dlang.org/download). The following versions or higher 61 | 62 | - dmd v2.086.1 or ldc v1.16.0 (gdc may work if the front end is above 2.086.1 but is unsupported) 63 | - dub v1.14.0 64 | 65 | If your system uses version 1.1 of OpenSSL (such as Fedora) use *VibeUseOpenSSL11* in the _versions_ of `dub.json` 66 | 67 | ### Build using dub 68 | 69 | Release builds can be done using the _default_ configuration. 70 | 71 | ```bash 72 | dub --compiler=ldc2 73 | ``` 74 | 75 | During development the _unittest_ configuration can be used if you only intend to use [sqlite](https://www.sqlite.org/). 76 | 77 | ```bash 78 | dub --config=unittest 79 | ``` 80 | 81 | ### Build dependencies (client side) 82 | 83 | The front end uses jQuery, Bootstrap 4, and Font Awesome. These dependencies are handled via [Bower](http://bower.io/) which can be installed using npm 84 | 85 | On Fedora nodejs and npm can be installed from the repository: 86 | 87 | ``` 88 | sudo dnf module enable nodejs:18 89 | sudo dnf module install nodejs:18/development 90 | ``` 91 | 92 | Then install yarn globally: 93 | 94 | ``` 95 | sudo npm install -g yarn 96 | ``` 97 | 98 | There's no need to do a `bower install` as this will be done by dub during the build process. 99 | 100 | ### Build dependencies (server side) 101 | 102 | You will also need libevent on your system 103 | 104 | Ubuntu: 105 | 106 | ``` 107 | sudo apt-get install libevent-dev libsqlite3-dev libpq-dev 108 | ``` 109 | 110 | Fedora: 111 | 112 | ``` 113 | sudo dnf install libevent-devel openssl-devel sqlite-devel postgresql-devel unixODBC-devel 114 | ``` 115 | 116 | OSX: 117 | 118 | ``` 119 | brew install libev libevent sqlite 120 | ``` 121 | 122 | ## Setting up the database 123 | 124 | ### Install Maria DB 125 | 126 | Ubuntu: `sudo apt-get install mariadb-client mariadb-server` 127 | Fedora: `sudo dnf install mariadb mariadb-server` 128 | 129 | ### Create the database and a user 130 | 131 | ``` 132 | CREATE DATABASE eloquent; 133 | CREATE USER 'dbuser'@'localhost' IDENTIFIED BY 'passw0rd'; 134 | GRANT ALL PRIVILEGES ON eloquent.* TO 'dbuser'@'localhost'; 135 | FLUSH PRIVILEGES; 136 | ``` 137 | 138 | ### Then import a SQL dump of a wordpress installation 139 | 140 | ``` 141 | mysql -u dbuser -p eloquent < wordpress-bak.sql 142 | ``` 143 | 144 | ## Running behind Apache 145 | 146 | ### Make sure you have Apache installed: 147 | 148 | 149 | ``` 150 | sudo apt-get install apache2 151 | ``` 152 | 153 | ### Configure a Virtual Host for use with the application `sudo vim /etc/apache2/sites-available/eloquent.conf` using the following content: 154 | 155 | ``` 156 | 157 | ServerAdmin webmaster@example.com 158 | ServerName example.com 159 | ServerAlias www.example.com 160 | 161 | ErrorLog /var/log/apache2/mysite-error.log 162 | CustomLog /var/log/apache2/mysite-access.log common 163 | 164 | ProxyRequests off 165 | ProxyPreserveHost off 166 | ProxyPass / http://127.0.0.1:8080 167 | ProxyPassReverse / http://127.0.0.1:8080 168 | 169 | 170 | ``` 171 | 172 | ### Then make sure to enable the relevant mods and restart Apache 173 | 174 | ``` 175 | sudo vim /etc/apache2/sites-available/eloquent.conf 176 | sudo a2enmod proxy proxy_http 177 | sudo a2ensite eloquent 178 | ``` 179 | 180 | ### Check the config and restart 181 | 182 | Verfiy the Apache config with `apachectl configtest` then restart: 183 | 184 | 185 | ``` 186 | sudo systemctl restart apache2 187 | ``` 188 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eloquent", 3 | "description": "Eloquent is a blogging application written in D.", 4 | "copyright": "Copyright © 2015, Samael", 5 | "authors": ["Samael"], 6 | "homepage": "https://singingbush.com", 7 | "license": "GPL-2.0", 8 | "dependencies": { 9 | "proper-d": "~>0.0.2", 10 | "colourfulmoon": "~>1.0.2", 11 | "vibe-core": "~>2.4.0", 12 | "vibe-d:web": "~>0.9.7", 13 | "vibe-d:tls": "~>0.9.7", 14 | "sha3-d": "~>1.0.0", 15 | "poodinis": "~>9.0.0", 16 | "hibernated": "~>0.3.10" 17 | }, 18 | "lflags-osx": ["-L/usr/local/opt/openssl/lib", "-L/usr/local/opt/sqlite/lib"], 19 | "stringImportPaths": ["views", "i18n"], 20 | "targetType": "executable", 21 | "buildRequirements": [ 22 | "allowWarnings" 23 | ], 24 | "toolchainRequirements": { 25 | "dub": ">=1.14.0", 26 | "frontend": ">=2.097" 27 | }, 28 | "preGenerateCommands-posix": [ 29 | "if ! type \"npm\" > /dev/null; then echo -e \"You need to have Node.js and npm installed in order to build this project\nvisit https://nodejs.org for more info\"; exit 1; fi", 30 | "if ! type \"yarn\" > /dev/null; then echo -e \"You need to have yarn installed in order to build this project\nvisit https://yarnpkg.com for more info\"; exit 1; fi", 31 | "printf \"\\033[94mInstalling front end dependencies with yarn...\\033[0m\n\"", 32 | "yarn install", 33 | "mkdir -p ./public/js ./public/css ./public/fonts", 34 | "rsync -u node_modules/@popperjs/core/dist/umd/popper.min.js ./public/js/popper.min.js", 35 | "rsync -u node_modules/bootstrap/dist/css/bootstrap.min.css ./public/css/bootstrap.min.css", 36 | "rsync -u node_modules/bootstrap/dist/js/bootstrap.min.js ./public/js/bootstrap.min.js", 37 | "rsync -u node_modules/font-awesome/css/font-awesome.min.css ./public/css/font-awesome.min.css", 38 | "rsync -u node_modules/font-awesome/fonts/fontawesome-webfont.* ./public/fonts/", 39 | "echo client-side dependencies done." 40 | ], 41 | "preGenerateCommands-windows": [ 42 | "where /q npm || echo \"You need to have Node.js and npm installed in order to build this project\nvisit https://nodejs.org for more info\"", 43 | "where /q yarn || echo \"You need to have yarn installed in order to build this project\nvisit https://yarnpkg.com for more info\"; exit 1; fi", 44 | "echo Installing front end dependencies with yarn...", 45 | "yarn install", 46 | "if not exist public\\js md public\\js", 47 | "if not exist public\\css md public\\css", 48 | "if not exist public\\fonts md public\\fonts", 49 | "if not exist /public/js/popper.min.js copy node_modules\\@popperjs\\core\\dist\\umd\\popper.min.js public\\js\\popper.min.js", 50 | "if not exist /public/css/bootstrap.min.css copy node_modules\\bootstrap\\dist\\css\\bootstrap.min.css public\\css\\bootstrap.min.css", 51 | "if not exist /public/js/bootstrap.min.js copy node_modules\\bootstrap\\dist\\js\\bootstrap.min.js public\\js\\bootstrap.min.js", 52 | "if not exist /public/css/font-awesome.min.css copy node_modules\\font-awesome\\css\\font-awesome.min.css public\\css\\font-awesome.min.css", 53 | "if not exist /public/fonts/fontawesome-webfont.woff copy node_modules\\font-awesome\\fonts\\fontawesome-webfont.* public\\fonts\\", 54 | "echo client-side dependencies done." 55 | ], 56 | "preBuildCommands-posix": ["printf \"\\033[93mtodo\\033[0m get SASS compiler working as part of build\n\""], 57 | "configurations": [ 58 | { 59 | "name": "default", 60 | "versions": ["PRODUCTION", "USE_MYSQL", "USE_SQLITE", "USE_PGSQL"], 61 | "subConfigurations": { 62 | "vibe-d:core": "vibe-core", 63 | "hibernated": "full", 64 | "ddbc": "full" 65 | }, 66 | "buildRequirements": ["silenceWarnings"] 67 | }, 68 | { 69 | "name": "mysql", 70 | "versions": ["PRODUCTION", "USE_MYSQL"], 71 | "subConfigurations": { 72 | "vibe-d:core": "vibe-core", 73 | "hibernated": "MySQL", 74 | "ddbc": "MySQL" 75 | }, 76 | "buildRequirements": ["silenceWarnings"] 77 | }, 78 | { 79 | "name": "unittest", 80 | "versions": ["DEVELOPMENT", "USE_SQLITE"], 81 | "libs-posix": ["sqlite3"], 82 | "libs-windows": ["sqlite3"], 83 | "subConfigurations": { 84 | "vibe-d:core": "vibe-core", 85 | "hibernated": "SQLite", 86 | "ddbc": "SQLite" 87 | }, 88 | "buildRequirements": ["silenceWarnings", "silenceDeprecations"] 89 | } 90 | ], 91 | "versions": ["VibeDefaultMain", "Have_vibe_d_core"] 92 | } 93 | -------------------------------------------------------------------------------- /eloquent.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "follow_symlinks": true, 6 | "path": ".", 7 | "folder_exclude_patterns": [".dub", ".idea"], 8 | "file_exclude_patterns": ["eloquent", "__test__library__"] 9 | } 10 | ], 11 | 12 | "settings": {"tab_size": 4}, 13 | 14 | "build_systems": 15 | [ 16 | { 17 | "name": "dub", 18 | "cmd": ["dub test"] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /i18n/text.de_DE.po: -------------------------------------------------------------------------------- 1 | # 2 | # The msgctxt must match the file of the diet template the message will be used in 3 | # 4 | 5 | 6 | ## Messages for index.dt 7 | 8 | msgctxt "index" 9 | msgid "home.welcome-message" 10 | msgstr "Startseite" 11 | 12 | msgctxt "index" 13 | msgid "txt.read-more" 14 | msgstr "Read More" 15 | 16 | 17 | ## Messages for profile.dt 18 | 19 | msgctxt "profile" 20 | msgid "txt.read-more" 21 | msgstr "Read More" -------------------------------------------------------------------------------- /i18n/text.en_GB.po: -------------------------------------------------------------------------------- 1 | # 2 | # The msgctxt must match the file of the diet template the message will be used in 3 | # 4 | 5 | 6 | ## Messages for index.dt 7 | 8 | msgctxt "index" 9 | msgid "home.welcome-message" 10 | msgstr "Home Page" 11 | 12 | msgctxt "index" 13 | msgid "txt.read-more" 14 | msgstr "Read More" 15 | 16 | 17 | ## Messages for profile.dt 18 | 19 | msgctxt "profile" 20 | msgid "txt.read-more" 21 | msgstr "Read More" -------------------------------------------------------------------------------- /i18n/text.en_US.po: -------------------------------------------------------------------------------- 1 | # 2 | # The msgctxt must match the file of the diet template the message will be used in 3 | # 4 | 5 | 6 | ## Messages for index.dt 7 | 8 | msgctxt "index" 9 | msgid "home.welcome-message" 10 | msgstr "Home Page" 11 | 12 | msgctxt "index" 13 | msgid "txt.read-more" 14 | msgstr "Read More" 15 | 16 | 17 | ## Messages for profile.dt 18 | 19 | msgctxt "profile" 20 | msgid "txt.read-more" 21 | msgstr "Read More" -------------------------------------------------------------------------------- /i18n/text.ru_RU.po: -------------------------------------------------------------------------------- 1 | # 2 | # The msgctxt must match the file of the diet template the message will be used in 3 | # 4 | 5 | 6 | ## Messages for index.dt 7 | 8 | msgctxt "index" 9 | msgid "home.welcome-message" 10 | msgstr "Добро пожаловать" 11 | 12 | msgctxt "index" 13 | msgid "txt.read-more" 14 | msgstr "Read More" 15 | 16 | 17 | ## Messages for profile.dt 18 | 19 | msgctxt "profile" 20 | msgid "txt.read-more" 21 | msgstr "Read More" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@popperjs/core": "^2.11.7", 4 | "bootstrap": "^5.3.0", 5 | "font-awesome": "4.7.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /admin/ 3 | -------------------------------------------------------------------------------- /public/styles/style.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | font-family: 'Droid Sans','Ubuntu','Open Sans',-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif; 4 | } 5 | 6 | /*nav {*/ 7 | /*margin-bottom: 1.5rem;*/ 8 | /*}*/ 9 | 10 | .login-row { 11 | margin: 10% -15px; 12 | } 13 | 14 | .bg-shadow { 15 | box-shadow: inset 0 0 5rem rgba(0,0,0,.5); 16 | color: #fff; 17 | text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); 18 | } -------------------------------------------------------------------------------- /schema.sql: -------------------------------------------------------------------------------- 1 | -- sqlite> .read schema.sql 2 | -- or we could create a sqlite db file using dub preBuildCommands: "sqlite3 testdb.sqlite < schema.sql" 3 | 4 | DROP TABLE IF EXISTS wp_commentmeta; 5 | DROP TABLE IF EXISTS wp_comments; 6 | DROP TABLE IF EXISTS wp_options; 7 | DROP TABLE IF EXISTS wp_postmeta; 8 | DROP TABLE IF EXISTS wp_posts; 9 | DROP TABLE IF EXISTS wp_usermeta; 10 | DROP TABLE IF EXISTS wp_users; 11 | 12 | CREATE TABLE wp_commentmeta( 13 | meta_id INTEGER PRIMARY KEY AUTOINCREMENT, 14 | comment_id INTEGER NOT NULL DEFAULT 0, 15 | meta_key varchar(255), 16 | meta_value longtext 17 | ); 18 | 19 | CREATE TABLE wp_comments( 20 | comment_ID INTEGER PRIMARY KEY AUTOINCREMENT, 21 | comment_post_ID INTEGER DEFAULT 0, 22 | comment_author tinytext, 23 | comment_author_email varchar(100), 24 | comment_author_url varchar(200), 25 | comment_author_IP varchar(100), 26 | comment_date datetime DEFAULT '0000-00-00 00:00:00', 27 | comment_date_gmt datetime DEFAULT '0000-00-00 00:00:00', 28 | comment_content text, 29 | comment_karma int(11) DEFAULT 0, 30 | comment_approved varchar(20) DEFAULT 1, 31 | comment_agent varchar(255), 32 | comment_type varchar(20), 33 | comment_parent INTEGER DEFAULT 0, 34 | user_id INTEGER DEFAULT 0 35 | ); 36 | 37 | CREATE TABLE wp_options( 38 | option_id INTEGER PRIMARY KEY AUTOINCREMENT, 39 | option_name varchar(64), 40 | option_value longtext, 41 | autoload varchar(20) DEFAULT 'YES' 42 | ); 43 | 44 | 45 | CREATE TABLE wp_postmeta( 46 | meta_id INTEGER PRIMARY KEY AUTOINCREMENT, 47 | post_id INTEGER DEFAULT 0, 48 | meta_key varchar(255), 49 | meta_value longtext 50 | ); 51 | 52 | CREATE TABLE wp_posts( 53 | ID INTEGER PRIMARY KEY AUTOINCREMENT, 54 | post_author INTEGER DEFAULT 0, 55 | post_date datetime NOT NULL DEFAULT '0000-00-00 00:00:00', 56 | post_modified datetime NOT NULL DEFAULT '0000-00-00 00:00:00', 57 | post_content longtext NOT NULL, 58 | post_title text NOT NULL, 59 | post_excerpt text NOT NULL, 60 | post_type varchar(20) NOT NULL, 61 | post_status varchar(20) DEFAULT 'publish', 62 | comment_status varchar(20) DEFAULT 'open', 63 | ping_status varchar(20) DEFAULT 'open' 64 | ); 65 | 66 | CREATE TABLE wp_usermeta( 67 | umeta_id INTEGER PRIMARY KEY AUTOINCREMENT, 68 | user_id INTEGER DEFAULT 0, 69 | meta_key varchar(255), 70 | meta_value longtext 71 | ); 72 | 73 | CREATE TABLE wp_users( 74 | ID INTEGER PRIMARY KEY AUTOINCREMENT, 75 | user_login varchar(60), 76 | user_pass varchar(64), 77 | user_nicename varchar(50), 78 | user_email varchar(100), 79 | user_url varchar(100), 80 | user_registered datetime DEFAULT '0000-00-00 00:00:00', 81 | user_activation_key varchar(60), 82 | user_status int(11) DEFAULT 0, 83 | display_name varchar(250) NULL 84 | ); 85 | 86 | INSERT INTO wp_users(user_login, user_pass, user_nicename, display_name, user_email, user_url, user_registered) 87 | VALUES 88 | ('admin', 'password', 'Nice Name', 'Display Name', 'user@domain.com', NULL, CURRENT_TIMESTAMP), 89 | ('user', 'password', 'Administrator', 'Display Name', 'admin@domain.com', NULL, CURRENT_TIMESTAMP); 90 | 91 | INSERT INTO wp_usermeta(user_id, meta_key, meta_value) 92 | VALUES 93 | (2, 'wp_capabilities', 'a:1:{s:13:"administrator";s:1:"1";}'), 94 | (2, 'wp_user_level', 10); -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SingingBush/eloquent/3597cf19f51120034f0d0d3b673112d4262b1252/screenshot.png -------------------------------------------------------------------------------- /source/app.d: -------------------------------------------------------------------------------- 1 | 2 | import poodinis.container : DependencyContainer; 3 | import poodinis.context : registerContext; 4 | 5 | import vibe.core.log; 6 | import vibe.http.fileserver : serveStaticFiles; 7 | import vibe.http.router : URLRouter; 8 | 9 | import eloquent.config.properties; 10 | import eloquent.config.context; 11 | import eloquent.controllers; 12 | 13 | shared static this() { 14 | auto container = new shared DependencyContainer(); 15 | container.registerContext!PoodinisContext; // Create application context before doing anything else 16 | 17 | Properties properties = container.resolve!Properties; 18 | 19 | auto router = new URLRouter; 20 | 21 | router 22 | .any("*", delegate(req, res) { 23 | req.params["version"] = "1.0-SNAPSHOT"; 24 | }) 25 | .get("*", serveStaticFiles("public/")) 26 | .registerWebInterface(container.resolve!WebappController); 27 | 28 | auto adminSettings = new WebInterfaceSettings; 29 | adminSettings.urlPrefix = "/admin"; 30 | router.registerWebInterface(container.resolve!AdminController, adminSettings); 31 | 32 | auto settings = new HTTPServerSettings; 33 | settings.port = properties.as!(ushort)("http.port", 80); 34 | settings.bindAddresses = ["::1", "127.0.0.1"]; 35 | settings.sessionStore = new MemorySessionStore; 36 | settings.errorPageHandler = delegate(req, res, error) { 37 | CurrentUser user; // kludge for getting template to render when serving error page 38 | render!("error.dt", req, error, user)(res); 39 | }; 40 | 41 | listenHTTP(settings, router); 42 | 43 | logInfo("Eloquent server ready..."); 44 | } 45 | 46 | shared static ~this() { 47 | logInfo("Application shutting down - goodbye!"); // see also logError and logDiagnostic 48 | } 49 | 50 | struct CurrentUser { 51 | string username = null; 52 | bool authenticated = false; 53 | bool administrator = false; 54 | } 55 | -------------------------------------------------------------------------------- /source/config/context.d: -------------------------------------------------------------------------------- 1 | module eloquent.config.context; 2 | 3 | private import hibernated.core : SessionFactory, SessionFactoryImpl; 4 | 5 | private import poodinis.context : ApplicationContext; 6 | private import poodinis.container : RegistrationOption, DependencyContainer; 7 | private import poodinis.registration; 8 | 9 | private import vibe.core.log; // only the logger is needed 10 | 11 | private import eloquent.config.properties, eloquent.config.database, eloquent.config.logging, eloquent.config.motd; 12 | private import eloquent.controllers; 13 | private import eloquent.services; 14 | 15 | class PoodinisContext : ApplicationContext { 16 | 17 | private Properties _properties; 18 | 19 | public this() { 20 | displayBanner(); 21 | } 22 | 23 | public override void registerDependencies(shared(DependencyContainer) container) { 24 | Properties properties = new Properties; 25 | configureLogging(properties); 26 | logInfo("Registering Dependencies with Poodinis Context"); 27 | container.register!Properties.existingInstance(properties); 28 | 29 | container.register!(EloquentDatabase, EloquentDatabaseImpl); 30 | EloquentDatabase dbConfig = container.resolve!EloquentDatabase; 31 | SessionFactoryImpl sessionFactory = dbConfig.configure(); 32 | container.register!(SessionFactory, SessionFactoryImpl)(RegistrationOption.doNotAddConcreteTypeRegistration).existingInstance(sessionFactory); 33 | container.register!(UserService, UserServiceImpl)(RegistrationOption.doNotAddConcreteTypeRegistration); 34 | container.register!(BlogService, BlogServiceImpl)(RegistrationOption.doNotAddConcreteTypeRegistration); 35 | 36 | // register Controllers (used as vibe-d WebInterface) 37 | container.register!WebappController; 38 | container.register!AdminController; 39 | } 40 | 41 | // @Component 42 | // public SessionFactory createSessionFactory() { 43 | // EloquentDatabase dbConfig = new EloquentDatabase; 44 | // return dbConfig.configure(); 45 | // } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /source/config/database.d: -------------------------------------------------------------------------------- 1 | module eloquent.config.database; 2 | 3 | private import hibernated.core : Connection, ConnectionPoolDataSourceImpl, DataSource, Dialect, EntityMetaData, SchemaInfoImpl, Session, SessionFactoryImpl; 4 | 5 | private import poodinis.autowire: Autowire; 6 | private import vibe.core.log; // only the logger is needed 7 | 8 | private import eloquent.config.properties; 9 | private import eloquent.model.user, eloquent.model.blogpost, eloquent.model.comment; 10 | 11 | interface EloquentDatabase { 12 | SessionFactoryImpl configure(); 13 | } 14 | 15 | class EloquentDatabaseImpl : EloquentDatabase { 16 | 17 | @Autowire 18 | private Properties _properties; 19 | 20 | public this() { 21 | logInfo("configuring Eloquent database"); 22 | } 23 | 24 | //@Component 25 | SessionFactoryImpl configure() { 26 | 27 | DataSource dataSource; 28 | Dialect dialect; 29 | 30 | immutable string dbType = _properties.as!(string)("db.dialect", "SQLite"); 31 | 32 | static if(__VERSION__ < 2085) { 33 | import std.regex : toUpper; 34 | } else { 35 | import std.string : toUpper; // previously had: import std.uni : toUpper; 36 | } 37 | 38 | final switch(dbType.toUpper) { 39 | case "SQLITE": 40 | version(USE_SQLITE) { 41 | import hibernated.core : SQLiteDialect; 42 | dataSource = createSQLiteDataSource(); 43 | dialect = new SQLiteDialect(); 44 | } else { 45 | logError("DB configured for SQLite dialect but SQLite was not enabled in the build"); 46 | } 47 | break; 48 | case "MYSQL": 49 | version(USE_MYSQL) { 50 | import hibernated.core : MySQLDialect; 51 | dataSource = createMySQLDataSource(); 52 | dialect = new MySQLDialect(); 53 | } else { 54 | logError("DB configured for MySQL dialect but MySQL was not enabled in the build"); 55 | } 56 | break; 57 | } 58 | 59 | logDebug("Creating schema meta data from annotations..."); 60 | EntityMetaData schema = new SchemaInfoImpl!(User, UserData, BlogPost, BlogPostData, Comment, CommentData); 61 | 62 | logDebug("Creating session factory..."); 63 | SessionFactoryImpl factory = new SessionFactoryImpl(schema, dialect, dataSource); 64 | 65 | immutable bool createSchema = _properties.as!(bool)("db.createSchema", false); 66 | if(createSchema) { 67 | Connection conn = dataSource.getConnection(); 68 | scope(exit) conn.close(); 69 | // create tables if not exist 70 | logInfo("Creating database tables..."); 71 | factory.getDBMetaData().updateDBSchema(conn, true, true); // bools are: dropTables, createTables 72 | } 73 | 74 | immutable bool createTestData = _properties.as!(bool)("db.createTestData", false); 75 | if(createTestData) { 76 | Session session = factory.openSession(); 77 | scope(exit) session.close(); 78 | 79 | import std.datetime; 80 | immutable SysTime now = Clock.currTime(UTC()); 81 | 82 | User user = new User; 83 | user.username = "test"; 84 | 85 | //string salt = _properties.as!(string)("auth.salt"); // todo: use salt again? 86 | 87 | // SHA 3: 88 | import sha3d.sha3 : sha3_256Of; 89 | import std.digest : toHexString; 90 | 91 | user.pass = toHexString(sha3_256Of("password")); 92 | 93 | user.nicename = "Ben"; 94 | user.displayname = "Benny"; 95 | user.email = "test@domain.com"; 96 | user.url = ""; 97 | user.registered = cast(DateTime) now; 98 | user.status = UserStatus.DEFAULT; 99 | session.save(user); 100 | logInfo("Created user: %s", user); 101 | 102 | User adminUser = new User; 103 | adminUser.username = "admin"; 104 | adminUser.pass = toHexString(sha3_256Of("password")); 105 | adminUser.nicename = "Administrator"; 106 | adminUser.displayname = "Administrator"; 107 | adminUser.email = "admin@domain.com"; 108 | adminUser.url = ""; 109 | adminUser.registered = cast(DateTime) now; 110 | adminUser.status = UserStatus.DEFAULT; 111 | session.save(adminUser); 112 | logInfo("Created user: %s", adminUser); 113 | 114 | UserData adminMetaData = new UserData; 115 | adminMetaData.user = adminUser; 116 | adminMetaData.key = "wp_user_level"; 117 | adminMetaData.value = "10"; 118 | session.save(adminMetaData); 119 | 120 | BlogPost bp = new BlogPost; 121 | bp.author = user; 122 | bp.created = cast(DateTime) now; 123 | bp.modified = cast(DateTime) now; 124 | bp.title = "Lorem ipsum"; 125 | bp.content = "Lorem ipsum dolor sit amet, ius eu suscipit honestatis consequuntur, velit cotidieque at eam."; 126 | bp.excerpt = "ius eu suscipit honestatis consequuntur"; 127 | bp.type = "post"; 128 | session.save(bp); 129 | logInfo("Created BlogPost: %s", bp); 130 | } 131 | return factory; 132 | } 133 | 134 | version(USE_SQLITE) { 135 | private DataSource createSQLiteDataSource() { 136 | auto sqliteFile = _properties.as!(string)("db.file"); 137 | logInfo("PoodinisContext -> loading SQLite file... %s", sqliteFile); 138 | import ddbc.drivers.sqliteddbc : SQLITEDriver; 139 | return new ConnectionPoolDataSourceImpl(new SQLITEDriver(), sqliteFile, null); 140 | } 141 | } 142 | 143 | version(USE_MYSQL) { 144 | private DataSource createMySQLDataSource() { 145 | auto dbHost = _properties.as!(string)("db.domain", "localhost"); 146 | auto dbPort = _properties.as!(ushort)("db.port", 3306); 147 | auto dbName = _properties.as!(string)("db.name"); 148 | auto dbUser = _properties.as!(string)("db.user"); 149 | auto dbPass = _properties.as!(string)("db.password"); 150 | 151 | logInfo("PoodinisContext -> connecting to MySQL... %s@%s:%s/%s", dbUser, dbHost, dbPort, dbName); 152 | 153 | import ddbc.drivers.mysqlddbc : MySQLDriver; 154 | string url = MySQLDriver.generateUrl(dbHost, dbPort, dbName); 155 | string[string] params = MySQLDriver.setUserAndPassword(dbUser, dbPass); 156 | return new ConnectionPoolDataSourceImpl(new MySQLDriver(), url, params); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /source/config/logging/colorstdlogger.d: -------------------------------------------------------------------------------- 1 | module eloquent.config.logging.colorstdlogger; 2 | 3 | static if (__traits(compiles, (){ import std.logger; } )) { 4 | private import std.logger.core : Logger, LogLevel; 5 | private alias StdLogLevel = LogLevel; 6 | } else { 7 | private import std.experimental.logger : Logger, LogLevel; 8 | private alias StdLogLevel = LogLevel; 9 | } 10 | private import std.regex : matchFirst, replaceFirst, regex; 11 | private import std.stdio : write, writef, writeln; 12 | private import std.conv : to; 13 | 14 | 15 | // ------- requires ColourfulMoon 16 | private import ColourfulMoon; 17 | 18 | // colours used are from Twitter Bootstrap 19 | private auto Debug = Colour(92, 184, 92); 20 | private auto Info = Colour(51, 122, 183); 21 | private auto Warn = Colour(240, 173, 78); 22 | private auto Error = Colour(217, 83, 79); 23 | 24 | private import std.concurrency : Tid; 25 | private import std.datetime.systime : SysTime; 26 | 27 | class StdColourfulMoonLogger : Logger { 28 | 29 | this(const StdLogLevel lv = StdLogLevel.all) @safe { 30 | super(lv); 31 | } 32 | 33 | override protected void beginLogMsg(string file, int line, string funcName, 34 | string prettyFuncName, string moduleName, StdLogLevel logLevel, 35 | Tid threadId, SysTime timestamp, Logger logger) @trusted { 36 | string level = "UNKNOWN"; 37 | auto fg = Colour(); 38 | 39 | switch (logLevel) { 40 | case StdLogLevel.trace: 41 | level = "TRACE"; 42 | fg = Debug; 43 | break; 44 | case StdLogLevel.info: 45 | level = "INFO"; 46 | fg = Info; 47 | break; 48 | case StdLogLevel.warning: 49 | level = "WARN"; 50 | fg = Warn; 51 | break; 52 | case StdLogLevel.error: 53 | level = "ERROR"; 54 | fg = Error; 55 | break; 56 | case StdLogLevel.critical: 57 | level = "CRITICAL"; 58 | fg = Error; 59 | break; 60 | case StdLogLevel.fatal: 61 | level = "FATAL"; 62 | fg = Error; 63 | break; 64 | default: 65 | level = logLevel.to!string; 66 | break; 67 | } 68 | 69 | // note that I don't use 'write(msg.time)' here because it doesn't output correct time (I'm currently in BST) 70 | //import std.datetime : Clock; 71 | //write(Clock.currTime()); // could also use: write(Clock.currTime().toISOExtString()); 72 | write(timestamp); 73 | 74 | writef(" - %s", threadId); 75 | 76 | write(" ["); 77 | level.Foreground(fg).Reset.write; 78 | write("] "); 79 | 80 | file = replaceFirst(file, regex(r".*\.?dub(\\|\/)packages(\\|\/)"), ""); // don't show path to local dub repo 81 | 82 | auto cyan = Colour(80, 238, 238); 83 | file.Foreground(cyan).Reset.write; 84 | write("("); 85 | 86 | (to!string(line)).Foreground(cyan).Reset.write; 87 | write("): "); 88 | 89 | // alternative:: 90 | // auto ltw = stdout.lockingTextWriter(); 91 | // import std.format : formattedWrite; 92 | // formattedWrite(ltw, "%s - %s [%s] %s(%u): ", timestamp, threadId, level, file, line); 93 | } 94 | 95 | override protected void logMsgPart(scope const(char)[] msg) @trusted { 96 | write(replaceFirst(msg, regex(r".*\.?dub(\\|\/)packages(\\|\/)"), "")); 97 | 98 | // auto ltw = stdout.lockingTextWriter(); 99 | // import std.format : formattedWrite; 100 | // formattedWrite(ltw, replaceFirst(msg, regex(r".*\.?dub(\\|\/)packages(\\|\/)"), "")); 101 | } 102 | 103 | override protected void finishLogMsg() @trusted { 104 | writeln(); 105 | } 106 | 107 | override void writeLogMsg(ref LogEntry payload) @safe { 108 | this.beginLogMsg(payload.file, payload.line, payload.funcName, 109 | payload.prettyFuncName, payload.moduleName, payload.logLevel, 110 | payload.threadId, payload.timestamp, payload.logger); 111 | 112 | this.logMsgPart(payload.msg); 113 | 114 | this.finishLogMsg(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /source/config/logging/colorvibelogger.d: -------------------------------------------------------------------------------- 1 | module eloquent.config.logging.colorvibelogger; 2 | 3 | private import vibe.core.log : Logger, FileLogger, setLogFormat, setLogFile, setLogLevel, registerLogger, logInfo, LogLevel; 4 | private alias VibeLogger = Logger; 5 | private alias VibeLogLevel = LogLevel; 6 | 7 | private import std.regex : matchFirst, replaceFirst, regex; 8 | private import std.stdio : write, writef, writeln; 9 | private import std.conv : to; 10 | 11 | private import ColourfulMoon; 12 | 13 | // colours used are from Twitter Bootstrap 14 | private auto Debug = Colour(92, 184, 92); 15 | private auto Info = Colour(51, 122, 183); 16 | private auto Warn = Colour(240, 173, 78); 17 | private auto Error = Colour(217, 83, 79); 18 | 19 | /** 20 | * An implementation of vibe.core.log.Logger that provides multi-color output in supported terminals by using ColourfulMoon. 21 | * 22 | * Besides the usual option for setting a minimum LogLevel, there is also an option to set the level that applies 23 | * to logging that is being done from within vibe-d. This makes it possible to set the minimum to LogLevel.debugV 24 | * without lots of debug messages comming from vibe-d. The LogLevel for vibe-d is set to LogLevel.info by default. 25 | * 26 | * The file paths that get output in log lines are cleaned up making log output much more readable. 27 | * 28 | * Authors: Sam Bate 29 | * Date: June 25, 2017 30 | * See_Also: 31 | * https://github.com/azbukagh/ColourfulMoon 32 | */ 33 | final class ColourfulMoonLogger : VibeLogger { 34 | 35 | import vibe.core.log : LogLevel, LogLine; 36 | 37 | bool ignoreVibe = false; 38 | bool skip = false; 39 | 40 | this(LogLevel min = LogLevel.info, LogLevel vibeLevel = LogLevel.info) { 41 | minLevel = min; 42 | ignoreVibe = vibeLevel == LogLevel.none || vibeLevel > minLevel; 43 | } 44 | 45 | override void beginLine(ref LogLine msg) @trusted { 46 | if(ignoreVibe && matchFirst(msg.file, regex(r"(\\|\/)vibe-d(\\|\/)source(\\|\/)"))) { 47 | skip = true; 48 | return; 49 | } 50 | 51 | string level; 52 | auto fg = Colour(); 53 | 54 | final switch (msg.level) { 55 | case LogLevel.trace: 56 | level = "TRACE"; 57 | fg = Debug; 58 | break; 59 | case LogLevel.debugV: 60 | level = "VERBOSE"; 61 | fg = Debug; 62 | break; 63 | case LogLevel.debug_: 64 | level = "DEBUG"; 65 | fg = Debug; 66 | break; 67 | case LogLevel.diagnostic: 68 | level = "DIAGNOSTIC"; 69 | fg = Info; 70 | break; 71 | case LogLevel.info: 72 | level = "INFO"; 73 | fg = Info; 74 | break; 75 | case LogLevel.warn: 76 | level = "WARN"; 77 | fg = Warn; 78 | break; 79 | case LogLevel.error: 80 | level = "ERROR"; 81 | fg = Error; 82 | break; 83 | case LogLevel.critical: 84 | level = "CRITICAL"; 85 | fg = Error; 86 | break; 87 | case LogLevel.fatal: 88 | level = "FATAL"; 89 | fg = Error; 90 | break; 91 | case LogLevel.none: assert(false); 92 | } 93 | 94 | // note that I don't use 'write(msg.time)' here because it doesn't output correct time (I'm currently in BST) 95 | import std.datetime : Clock; 96 | write(Clock.currTime()); // could also use: write(Clock.currTime().toISOExtString()); 97 | 98 | writef(" - %08X", msg.threadID); 99 | 100 | if(msg.threadName !is null) { 101 | writef(":'%s'", msg.threadName); 102 | } else { 103 | writef(":%08X", msg.fiberID); 104 | } 105 | 106 | write(" ["); 107 | level.Foreground(fg).Reset.write; 108 | write("] "); 109 | 110 | string file = replaceFirst(msg.file, regex(r".*\.?dub(\\|\/)packages(\\|\/)"), ""); // don't show path to local dub repo 111 | 112 | auto cyan = Colour(80, 238, 238); 113 | file.Foreground(cyan).Reset.write; 114 | write("("); 115 | 116 | (to!string(msg.line)).Foreground(cyan).Reset.write; 117 | write("): "); 118 | } 119 | 120 | override void put(scope const(char)[] text) { 121 | if(!skip) { 122 | write(replaceFirst(text, regex(r".*\.?dub(\\|\/)packages(\\|\/)"), "")); 123 | } 124 | } 125 | 126 | override void endLine() { 127 | if(!skip) { 128 | writeln(); 129 | } 130 | skip = false; 131 | } 132 | } -------------------------------------------------------------------------------- /source/config/logging/package.d: -------------------------------------------------------------------------------- 1 | module eloquent.config.logging; 2 | 3 | private import eloquent.config.properties; 4 | private import eloquent.config.logging.colorstdlogger : StdColourfulMoonLogger; 5 | private import eloquent.config.logging.colorvibelogger : ColourfulMoonLogger; 6 | 7 | private import vibe.core.log : FileLogger, logInfo, LogLevel, registerLogger, setLogLevel, setLogFormat, setLogFile; 8 | private alias VibeLogLevel = LogLevel; 9 | 10 | static if (__traits(compiles, (){ import std.logger; } )) { 11 | private import std.logger.core : sharedLog; 12 | private alias StdLogLevel = std.logger.core.LogLevel; 13 | pragma(msg, "eloquent will redirect anything logged via 'std.logger.core : sharedLog' to StdColourfulMoonLogger."); 14 | } else static if(__traits(compiles, (){ import std.experimental.logger; } )) { 15 | private import std.experimental.logger : sharedLog; 16 | private alias StdLogLevel = std.experimental.logger.LogLevel; 17 | pragma(msg, "eloquent will redirect anything logged via 'std.experimental.logger : sharedLog' to StdColourfulMoonLogger."); 18 | } else { 19 | static assert(false, "neither std.logger or std.experimental.logger found"); 20 | } 21 | 22 | static if(__VERSION__ < 2085) { 23 | private import std.regex : toUpper; 24 | } else { 25 | private import std.string : toUpper; // previously had: import std.uni : toUpper; 26 | } 27 | 28 | private import std.stdio; 29 | private import std.conv : to; 30 | 31 | void configureLogging(Properties properties) { 32 | immutable auto logFile = properties.as!(string)("log.file", "eloquent-server.log"); 33 | immutable auto logLevel = properties.as!(string)("log.level", "info"); 34 | 35 | setLogFormat(FileLogger.Format.threadTime, FileLogger.Format.threadTime); // plain, thread, or threadTime 36 | 37 | VibeLogLevel level; 38 | 39 | switch(logLevel.toUpper) { 40 | case "VERBOSE": 41 | level = VibeLogLevel.debugV; 42 | break; 43 | case "DEBUG": 44 | level = VibeLogLevel.debug_; 45 | break; 46 | case "TRACE": 47 | level = VibeLogLevel.trace; 48 | break; 49 | case "ERROR": 50 | level = VibeLogLevel.error; 51 | break; 52 | case "WARN": 53 | level = VibeLogLevel.warn; 54 | break; 55 | case "INFO": 56 | default: 57 | level = VibeLogLevel.info; 58 | break; 59 | } 60 | 61 | setLogFile(logFile, level); 62 | 63 | // std logging 64 | static if (__traits(compiles, (){ import std.logger; } )) { 65 | switch(logLevel.toUpper) { 66 | case "VERBOSE": 67 | case "DEBUG": 68 | case "TRACE": 69 | sharedLog = (() @trusted => cast(shared)new StdColourfulMoonLogger(StdLogLevel.trace))(); 70 | break; 71 | case "ERROR": 72 | sharedLog = (() @trusted => cast(shared)new StdColourfulMoonLogger(StdLogLevel.error))(); 73 | break; 74 | case "WARN": 75 | sharedLog = (() @trusted => cast(shared)new StdColourfulMoonLogger(StdLogLevel.warning))(); 76 | break; 77 | case "INFO": 78 | default: 79 | sharedLog = (() @trusted => cast(shared)new StdColourfulMoonLogger(StdLogLevel.info))(); 80 | break; 81 | } 82 | } else static if(__traits(compiles, (){ import std.experimental.logger; } )) { 83 | switch(logLevel.toUpper) { 84 | case "VERBOSE": 85 | case "DEBUG": 86 | case "TRACE": 87 | sharedLog = new StdColourfulMoonLogger(StdLogLevel.trace); 88 | break; 89 | case "ERROR": 90 | sharedLog = new StdColourfulMoonLogger(StdLogLevel.error); 91 | break; 92 | case "WARN": 93 | sharedLog = new StdColourfulMoonLogger(StdLogLevel.warning); 94 | break; 95 | case "INFO": 96 | default: 97 | sharedLog = new StdColourfulMoonLogger(StdLogLevel.info); 98 | break; 99 | } 100 | } 101 | 102 | setLogLevel(VibeLogLevel.none); // this effectively deactivates vibe.d's stdout logger 103 | 104 | // now register a custom Logger that's nicer than the one provided by vibe.d (and outputs correct time) 105 | auto console = cast(shared)new ColourfulMoonLogger(level); 106 | registerLogger(console); 107 | 108 | logInfo("PoodinisContext -> Logging Configured [%s] will be output to: %s", logLevel, logFile); 109 | 110 | // todo: consider options for HTML Logger and SyslogLogger, see: https://github.com/rejectedsoftware/vibe.d/blob/master/source/vibe/core/log.d 111 | // auto logger = cast(shared)new HTMLLogger("log.html"); 112 | // registerLogger(logger); 113 | } 114 | 115 | -------------------------------------------------------------------------------- /source/config/motd.d: -------------------------------------------------------------------------------- 1 | module eloquent.config.motd; 2 | 3 | import std.stdio; 4 | import ColourfulMoon; 5 | 6 | void displayBanner() { 7 | auto cyan = Colour(80, 238, 238); 8 | ` 9 | ___________.__ __ 10 | \_ _____/| | ____ ________ __ ____ _____/ |_ 11 | | __)_ | | / _ \ / ____/ | \_/ __ \ / \ __\ 12 | | \| |_( <_> < <_| | | /\ ___/| | \ | 13 | /_______ /|____/\____/ \__ |____/ \___ >___| /__| 14 | \/ |__| \/ \/ 15 | `.Foreground(cyan).Reset.write; 16 | } 17 | -------------------------------------------------------------------------------- /source/config/properties.d: -------------------------------------------------------------------------------- 1 | module eloquent.config.properties; 2 | 3 | private import properd : readProperties, as; 4 | private import vibe.core.log; 5 | private import vibe.core.args; 6 | private import vibe.http.server : HTTPServerRequest; 7 | 8 | class Properties { 9 | 10 | private string[string] _properties; 11 | 12 | public this(string filePath = "./app.properties") { 13 | version(DEVELOPMENT) { 14 | logInfo("Properties -> configuring test properties"); 15 | _properties["http.port"] = "8080"; 16 | _properties["db.dialect"] = "SQLite"; 17 | _properties["db.file"] = "testdb.sqlite"; 18 | _properties["db.createSchema"] = "true"; 19 | _properties["db.createTestData"] = "true"; 20 | _properties["log.level"] = "debug"; 21 | } else { 22 | readOption("p|properties", &filePath, "path to properites file. Defaults to './app.properties'"); 23 | 24 | logInfo("Properties -> loading properties file: '%s'", filePath); 25 | _properties = readProperties(filePath); 26 | } 27 | } 28 | 29 | T as(T)(string name, T alternative=T.init) { 30 | return _properties.as!(T)(name, alternative); 31 | } 32 | } 33 | 34 | struct TranslationContext { 35 | private import std.typetuple : TypeTuple; 36 | alias languages = TypeTuple!("en_GB", "en_US", "de_DE", "ru_RU"); // first one is also the default 37 | 38 | private import vibe.web.web : translationModule, extractDeclStrings; 39 | mixin translationModule!"text"; 40 | 41 | static string determineLanguage(scope HTTPServerRequest req) { 42 | import std.string : split, replace; 43 | auto acc_lang = "Accept-Language" in req.headers; 44 | if(acc_lang) { 45 | return replace(split(*acc_lang, ",")[0], "-", "_"); 46 | } 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /source/controllers/admincontroller.d: -------------------------------------------------------------------------------- 1 | module eloquent.controllers.admin; 2 | 3 | import poodinis; 4 | import vibe.core.core; 5 | import vibe.http.router; 6 | import vibe.web.web; 7 | 8 | import eloquent.config.properties; 9 | import eloquent.services; 10 | import eloquent.controllers; 11 | 12 | // handles everything under '/admin' 13 | class AdminController : BaseController { 14 | 15 | @Autowire 16 | private Properties _properties; 17 | 18 | @Autowire 19 | private UserService _userService; 20 | 21 | @Autowire 22 | private BlogService _blogService; 23 | 24 | @admin 25 | @method(HTTPMethod.GET) 26 | void index(Json _user) { 27 | logInfo("GET: /admin"); 28 | 29 | immutable CurrentUser user = currentUser; 30 | 31 | render!("admin_users.dt", user); 32 | } 33 | 34 | @admin 35 | @method(HTTPMethod.GET) @path("/blogposts") 36 | void manageBlogposts(Json _user) { 37 | logInfo("GET: /admin/blogposts"); 38 | 39 | auto blogposts = _blogService.allBlogPosts(); 40 | 41 | immutable CurrentUser user = currentUser; 42 | 43 | render!("admin_blogposts.dt", blogposts, user); 44 | } 45 | 46 | @admin 47 | @method(HTTPMethod.GET) @path("/comments") 48 | void manageComments(Json _user) { 49 | logInfo("GET: /admin/comments"); 50 | 51 | auto comments = _blogService.getComments(); 52 | 53 | immutable CurrentUser user = currentUser; 54 | 55 | render!("admin_comments.dt", comments, user); 56 | } 57 | 58 | @auth 59 | @admin 60 | @method(HTTPMethod.GET) @path("/users") 61 | void getUsers(Json _user) { 62 | logInfo("GET: /admin/users"); 63 | 64 | immutable CurrentUser user = currentUser; 65 | 66 | render!("admin_users.dt", user); 67 | } 68 | 69 | @auth 70 | @admin 71 | @method(HTTPMethod.POST) @path("/user/create") 72 | void postUsers(Json _user, string username, string password, string email) { 73 | // string salt = _properties.as!(string)("auth.salt"); // todo: use salt again? 74 | 75 | // SHA 3: 76 | import sha3d.sha3 : sha3_256Of; 77 | import std.digest : toHexString; 78 | string hash = toHexString(sha3_256Of(password)); 79 | 80 | logInfo("POST: /admin/user/create: %s %s", username, email, hash); 81 | 82 | _userService.createUser(username, hash, email); 83 | 84 | redirect("/profile/%s".format(username)); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /source/controllers/package.d: -------------------------------------------------------------------------------- 1 | module eloquent.controllers; 2 | 3 | public import std.array, std.algorithm, std.string; 4 | public import eloquent.controllers.web, eloquent.controllers.admin; 5 | public import vibe.core.log, vibe.http.router, vibe.web.web, vibe.data.json; 6 | 7 | abstract class BaseController { 8 | 9 | protected { 10 | SessionVar!(CurrentUser, "user") currentUser; // stored in the session store 11 | } 12 | 13 | enum auth = before!ensureAuth("_user"); 14 | enum admin = before!ensureAdmin("_user"); 15 | 16 | Json ensureAuth(HTTPServerRequest req, HTTPServerResponse res) { 17 | logInfo("checking user is authenticated"); 18 | if(!currentUser.authenticated) { 19 | redirect("/login"); // throw new HTTPStatusException(401, "You need to be logged in"); 20 | } 21 | return serializeToJson(currentUser); 22 | } 23 | 24 | Json ensureAdmin(HTTPServerRequest req, HTTPServerResponse res) { 25 | logInfo("checking user is administrator"); 26 | if(!currentUser.authenticated) { 27 | redirect("/login"); 28 | } 29 | if(!currentUser.administrator) { 30 | throw new HTTPStatusException(401, "You do not have permission to access this resource"); 31 | } 32 | return serializeToJson(currentUser); 33 | } 34 | 35 | } 36 | 37 | struct CurrentUser { 38 | string username = null; 39 | bool authenticated = false; 40 | bool administrator = false; 41 | } 42 | -------------------------------------------------------------------------------- /source/controllers/webappcontroller.d: -------------------------------------------------------------------------------- 1 | module eloquent.controllers.web; 2 | 3 | import poodinis; 4 | import vibe.core.core; 5 | 6 | import eloquent.config.properties; 7 | import eloquent.model; 8 | import eloquent.services; 9 | import eloquent.controllers; 10 | 11 | // This is essentially like using Springs @Controller for handling routes in Spring MVC 12 | @translationContext!TranslationContext 13 | class WebappController : BaseController { 14 | 15 | @Autowire 16 | private Properties _properties; 17 | 18 | @Autowire 19 | private UserService _userService; 20 | 21 | @Autowire 22 | private BlogService _blogService; 23 | 24 | // GET / 25 | void index() { 26 | auto blogPosts = _blogService.allBlogPosts(); 27 | CurrentUser user = currentUser; 28 | render!("index.dt", blogPosts, user); 29 | } 30 | 31 | // @method(HTTPMethod.GET) @path("login") 32 | void getLogin(string _error = null) { 33 | CurrentUser user = currentUser; 34 | render!("login.dt", _error, user); 35 | } 36 | 37 | // POST /login (username and password are automatically read as form fields) 38 | @errorDisplay!getLogin 39 | void postLogin(string username, string password) { // todo: look at ValidUsername and ValidPassword structs in http://vibed.org/api/vibe.web.validation/ 40 | logInfo("User attempting to login: %s", username); 41 | 42 | enforceHTTP(username !is null && !username.empty, HTTPStatus.badRequest, "Username is a required field."); 43 | enforceHTTP(password !is null && !password.empty, HTTPStatus.badRequest, "Password is a required field."); 44 | 45 | // import vibe.utils.validation; 46 | // validateUserName(username) 47 | // validatePassword(password) 48 | 49 | // todo: create some real authentication 50 | auto user = _userService.findUser(username); 51 | 52 | logInfo("User retrieved from db: %s", user); 53 | 54 | enforceHTTP(user !is null, HTTPStatus.forbidden, "Invalid user name or password."); 55 | 56 | //string salt = _properties.as!(string)("auth.salt"); // todo: use salt again? 57 | 58 | // SHA 3: 59 | import sha3d.sha3 : sha3_256Of; 60 | import std.digest : toHexString; 61 | 62 | CurrentUser u; 63 | u.authenticated = (user.pass == toHexString(sha3_256Of(password))); 64 | 65 | enforceHTTP(u.authenticated, HTTPStatus.forbidden, "Invalid user name or password."); 66 | 67 | u.username = username; 68 | UserData[] data = user.data.find!(ud => ud.key == "wp_user_level"); 69 | if(data.length > 0) { 70 | u.administrator = data[0].value == "10"; 71 | } 72 | currentUser = u; 73 | 74 | //auto session = startSession(); 75 | //session.set("user", user); 76 | //logInfo("form: %s", form["password"]); 77 | 78 | redirect("/profile/%s".format(username)); 79 | } 80 | 81 | // POST /logout 82 | @method(HTTPMethod.GET) @path("logout") 83 | void getLogout() { 84 | immutable CurrentUser u; 85 | currentUser = u; 86 | terminateSession(); 87 | redirect("/"); 88 | } 89 | 90 | @auth 91 | @method(HTTPMethod.GET) @path("profile/:username") 92 | void getProfile(HTTPServerRequest req, Json _user, string _error = null, string _username = null) { 93 | string username = _username !is null? _username : currentUser.username; 94 | 95 | auto person = _userService.findUser(_username); 96 | 97 | if(person is null) { 98 | throw new HTTPStatusException(404, "Cannot find user: %s".format(username)); 99 | } 100 | auto blogPosts = _blogService.findAllByUser(person); 101 | 102 | immutable CurrentUser user = currentUser; 103 | render!("profile.dt", username, blogPosts, user); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /source/model/blogpost.d: -------------------------------------------------------------------------------- 1 | module eloquent.model.blogpost; 2 | 3 | import eloquent.model; 4 | 5 | enum PostType { 6 | page, 7 | post, 8 | revision, 9 | attachment, 10 | nav_menu_item 11 | } 12 | 13 | @Entity 14 | @Table("wp_posts") 15 | public class BlogPost { 16 | 17 | public: 18 | @Id 19 | @Generated 20 | uint id; // bigint (20) NOT NULL auto inc 21 | 22 | @ManyToOne @NotNull @JoinColumn("post_author") 23 | User author; 24 | 25 | @Column("post_date") @NotNull 26 | DateTime created; 27 | 28 | @Column("post_modified") @NotNull 29 | DateTime modified; 30 | 31 | @Column("post_content") @NotNull 32 | string content; // LONGTEXT 33 | 34 | @Column("post_title") @NotNull 35 | string title; // TEXT 36 | 37 | @Column("post_excerpt") @NotNull 38 | string excerpt; // TEXT 39 | 40 | @Column("post_type",20) @NotNull 41 | string type; 42 | 43 | @OneToMany 44 | LazyCollection!BlogPostData data; 45 | 46 | override string toString() { 47 | return format("{id:%s, %s, %s, author:%s, created:%s}", id, type, title, author.username, created); 48 | } 49 | } 50 | 51 | @Table("wp_postmeta") 52 | public class BlogPostData { 53 | 54 | public: 55 | @Column("meta_id", 20) // bigint(20) 56 | @Id 57 | @Generated 58 | uint id; 59 | 60 | @ManyToOne 61 | @JoinColumn("post_id") 62 | BlogPost blogPost; 63 | 64 | @Column("meta_key", 255) 65 | @Null 66 | string key; // VARCHAR(255) 67 | 68 | @Column("meta_value") 69 | @Null 70 | string value; // LONGTEXT 71 | 72 | } 73 | -------------------------------------------------------------------------------- /source/model/comment.d: -------------------------------------------------------------------------------- 1 | module eloquent.model.comment; 2 | 3 | import eloquent.model; 4 | 5 | //enum CommentType { 6 | //} 7 | 8 | @Entity 9 | @Table("wp_comments") 10 | public class Comment { 11 | 12 | public: 13 | @Id 14 | @Generated 15 | @Column("comment_ID") 16 | uint id; // bigint (20) NOT NULL auto inc 17 | 18 | @ManyToOne 19 | @JoinColumn("comment_post_ID") 20 | BlogPost blogPost; 21 | 22 | @Column("comment_author") 23 | string author; // tinytext 24 | 25 | @Column("comment_author_email") // varchar(100) 26 | string authorEmail; 27 | 28 | @Column("comment_author_url") // varchar(200) 29 | string authorUrl; 30 | 31 | @Column("comment_author_IP") // varchar(100) 32 | string authorIP; 33 | 34 | @Column("comment_date") 35 | DateTime created; 36 | 37 | @Column("comment_content") 38 | string content; // LONGTEXT 39 | 40 | @Column("comment_approved") // varchar(20) 41 | string approved; 42 | 43 | @Column("comment_agent") // varchar(255) 44 | string agent; 45 | 46 | @Column("comment_type") // varchar(20) 47 | string type; 48 | 49 | // @ManyToOne // hibernated doesnt like this 50 | // @JoinColumn("comment_parent") 51 | // Comment parent; 52 | 53 | // @ManyToOne // hibernated doesnt like this 54 | // @JoinColumn("user_id") 55 | // User user; 56 | 57 | @OneToMany 58 | LazyCollection!CommentData metadata; 59 | 60 | override string toString() { 61 | return format("{id:%s, %s, author:%s, created:%s, metadata:%s}", id, type, author, created, metadata); 62 | } 63 | } 64 | 65 | @Table("wp_commentmeta") 66 | public class CommentData { 67 | 68 | public: 69 | @Column("meta_id", 20) // bigint(20) 70 | @Id 71 | @Generated 72 | uint id; 73 | 74 | @ManyToOne 75 | @JoinColumn("comment_id") 76 | Comment user; 77 | 78 | @Column("meta_key", 255) 79 | @Null 80 | string key; // VARCHAR(255) 81 | 82 | @Column("meta_value") 83 | @Null 84 | string value; // LONGTEXT 85 | 86 | override string toString() { 87 | return format("{id:%s, key:%s, value:%s}", id, key, value); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /source/model/package.d: -------------------------------------------------------------------------------- 1 | module eloquent.model; 2 | 3 | public import hibernated.core; 4 | public import std.datetime, std.string; 5 | 6 | public import eloquent.model.user, eloquent.model.blogpost, eloquent.model.comment; 7 | -------------------------------------------------------------------------------- /source/model/user.d: -------------------------------------------------------------------------------- 1 | module eloquent.model.user; 2 | 3 | import eloquent.model; 4 | 5 | // supposedly the user_status field is a dead column in the wp_users table but it's still there 6 | enum UserStatus { 7 | DEFAULT, 8 | ONE,TWO,THREE,FOUR,FIVE // no idea what the status values are yet, need to investigate 9 | } 10 | 11 | @Entity 12 | @Table("wp_users") 13 | public class User { 14 | 15 | public: 16 | 17 | @Id 18 | @Generated 19 | uint id; // bigint (20) NOT NULL auto inc 20 | 21 | @Column("user_login",60) 22 | @NotNull 23 | string username; // user_login varchar(60) NOT NULL 24 | 25 | @Column("user_pass",64) 26 | @NotNull 27 | string pass; // user_pass varchar(64) NOT NULL 28 | 29 | @Column("user_nicename",50) 30 | @NotNull 31 | string nicename; // user_nicename varchar(50) NOT NULL 32 | 33 | @Column("display_name",250) 34 | @NotNull 35 | string displayname; // display_name varchar(250) NOT NULL 36 | 37 | @Column("user_status",11) 38 | @NotNull 39 | UserStatus status; // 40 | 41 | @Column("user_email",100) 42 | @NotNull 43 | string email; // user_email varchar(100) NOT NULL 44 | 45 | @Column("user_url",100) 46 | @NotNull 47 | string url; // user_url varchar(100) NOT NULL 48 | 49 | @Column("user_registered") 50 | @NotNull 51 | DateTime registered; // user_registered datetime NOT NULL 52 | 53 | @OneToMany 54 | UserData[] data; 55 | 56 | public: 57 | @Transient 58 | string getFormattedData() { 59 | return format("%s bits of meta data", data.length); 60 | } 61 | 62 | override string toString() { 63 | return format("{id:%s, username:%s, displayname:%s, status:%s}", id, username, displayname, status); 64 | } 65 | } 66 | 67 | @Table("wp_usermeta") // user meta data is essentially a key value store for user info 68 | public class UserData { 69 | 70 | public: 71 | @Column("umeta_id", 20) // bigint(20) 72 | @Id 73 | @Generated 74 | uint id; 75 | 76 | @ManyToOne 77 | @JoinColumn("user_id") 78 | User user; 79 | 80 | @Column("meta_key", 255) 81 | @Null 82 | string key; // VARCHAR(255) 83 | 84 | @Column("meta_value") 85 | @Null 86 | string value; // LONGTEXT 87 | } 88 | -------------------------------------------------------------------------------- /source/services/blogservice.d: -------------------------------------------------------------------------------- 1 | module eloquent.services.blogservice; 2 | 3 | import eloquent.model; 4 | import poodinis.autowire : Autowire; 5 | import vibe.core.log; 6 | 7 | interface BlogService { 8 | BlogPost[] allBlogPosts(); 9 | BlogPost getBlogPost(Uint id); 10 | Comment[] getComments(); 11 | BlogPost[] findRecentPosts(int amount); 12 | BlogPost[] findAllByUser(User user); 13 | } 14 | 15 | 16 | class BlogServiceImpl : BlogService { 17 | 18 | @Autowire 19 | private SessionFactory sessionFactory; 20 | 21 | 22 | public this() { 23 | logDebug("Creating DatabaseService"); 24 | } 25 | 26 | public ~this() { 27 | logInfo("closing SessionFactory: %s", sessionFactory); 28 | sessionFactory.close(); 29 | } 30 | 31 | public BlogPost[] allBlogPosts() { 32 | Session session = sessionFactory.openSession(); 33 | scope(exit) session.close(); 34 | 35 | Query q = session.createQuery("FROM BlogPost WHERE type='post' ORDER BY created DESC"); 36 | auto blogPosts = q.list!BlogPost(); 37 | //logInfo("posts %s", q.listRows()); // shows all params 38 | logDebug("BlogService - > found %s BlogPosts: %s", blogPosts.length, blogPosts); 39 | return blogPosts; 40 | } 41 | 42 | BlogPost getBlogPost(Uint id) { 43 | Session session = sessionFactory.openSession(); 44 | scope(exit) session.close(); 45 | 46 | auto blogPost = session.createQuery("FROM BlogPost WHERE type='post' AND id=:Id ORDER BY created DESC") 47 | .setParameter("Id", id) 48 | .uniqueResult!BlogPost; 49 | 50 | return blogPost; 51 | } 52 | 53 | Comment[] getComments() { 54 | Session session = sessionFactory.openSession(); 55 | scope(exit) session.close(); 56 | 57 | Query q = session.createQuery("FROM Comment ORDER BY created DESC"); 58 | auto comments = q.list!Comment(); 59 | 60 | // logInfo("comments %s", q.listRows()); // shows all params 61 | logDebug("BlogService - > found %s comments: %s", comments.length, comments); 62 | return comments; 63 | } 64 | 65 | // get the most recent posts with limit defined by method caller 66 | BlogPost[] findRecentPosts(int amount) { 67 | return null; // todo 68 | } 69 | 70 | BlogPost[] findAllByUser(User user) { 71 | Session session = sessionFactory.openSession(); 72 | scope(exit) session.close(); 73 | 74 | Query q = session.createQuery("FROM BlogPost WHERE type='post' AND author=:user_id ORDER BY created DESC") 75 | .setParameter("user_id", user.id); 76 | auto blogPosts = q.list!BlogPost(); 77 | //logInfo("posts %s", q.listRows()); // shows all params 78 | logDebug("BlogService - > found %s BlogPosts: %s", blogPosts.length, blogPosts); 79 | return blogPosts; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /source/services/package.d: -------------------------------------------------------------------------------- 1 | module eloquent.services; 2 | 3 | //public import std.datetime, std.conv, std.string; 4 | //public import std.algorithm : filter, startsWith; 5 | //public import std.array : array; 6 | 7 | public import eloquent.services.blogservice, eloquent.services.userservice; 8 | 9 | 10 | -------------------------------------------------------------------------------- /source/services/userservice.d: -------------------------------------------------------------------------------- 1 | module eloquent.services.userservice; 2 | 3 | import std.conv; 4 | 5 | import eloquent.model; 6 | import poodinis.autowire : Autowire; 7 | import vibe.core.log; 8 | 9 | interface UserService { 10 | User findUser(string username); 11 | User[] findUsers(); 12 | User createUser(string username, string passwdHash, string email); 13 | } 14 | 15 | 16 | class UserServiceImpl : UserService { 17 | 18 | @Autowire 19 | private SessionFactory sessionFactory; 20 | 21 | 22 | public this() { 23 | logDebug("Creating DatabaseService"); 24 | } 25 | 26 | public ~this() { 27 | logInfo("closing SessionFactory: %s", sessionFactory); 28 | sessionFactory.close(); 29 | } 30 | 31 | public User[] findUsers() { 32 | Session session = sessionFactory.openSession(); 33 | scope(exit) session.close(); 34 | 35 | logInfo("querying user table"); 36 | Query q = session.createQuery("FROM User ORDER BY nicename"); 37 | User[] results = q.list!User(); 38 | logInfo("results size is " ~ to!string(results.length)); 39 | 40 | return results; 41 | } 42 | 43 | public User findUser(string username) { 44 | Session session = sessionFactory.openSession(); 45 | scope(exit) session.close(); 46 | 47 | Query q = session 48 | .createQuery("FROM User WHERE username=:Username ORDER BY nicename") 49 | .setParameter("Username", username); 50 | return q.uniqueResult!User(); 51 | } 52 | 53 | public User createUser(string username, string passwdHash, string email) { 54 | Session session = sessionFactory.openSession(); 55 | scope(exit) session.close(); 56 | 57 | User user = new User; 58 | user.username = username; 59 | user.pass = passwdHash; 60 | user.nicename = ""; 61 | user.displayname = username; 62 | user.email = email; 63 | user.url = ""; 64 | import std.datetime; 65 | SysTime now = Clock.currTime(UTC()); 66 | user.registered = cast(DateTime) now; 67 | user.status = UserStatus.DEFAULT; 68 | session.save(user); 69 | logInfo("new user created: %s", user); 70 | return user; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /views/admin_blogposts.dt: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block title 4 | title Admin | Manage Blog Posts 5 | 6 | block body 7 | .jumbotron.jumbotron-primary 8 | .container 9 | h1 Manage Blog Posts 10 | 11 | .container(role='main') 12 | .row 13 | .col-md-12 14 | - foreach( p; blogposts ) 15 | h2 #{p.type} : #{p.title} 16 | p.lead Author #{p.author.displayname} (#{p.author.username}) 17 | p 18 | span.fa.fa-clock-o 19 | | #{p.created} 20 | hr 21 | p !{p.content} 22 | hr -------------------------------------------------------------------------------- /views/admin_comments.dt: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block title 4 | title Admin | Manage Comments 5 | 6 | block body 7 | .jumbotron.jumbotron-primary 8 | .container 9 | h1 Manage Comments 10 | 11 | .container(role='main') 12 | .row 13 | .col-md-12 14 | - foreach( c; comments ) 15 | h2 #{c.type} #{c.authorIP} 16 | p.lead Author #{c.author} 17 | p 18 | span.fa.fa-clock-o 19 | | #{c.created} 20 | hr 21 | p !{c.content} 22 | hr -------------------------------------------------------------------------------- /views/admin_users.dt: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block title 4 | title Admin | Manage Users 5 | 6 | block body 7 | .mb-4.bg-primary.bg-shadow 8 | .container.py-5 9 | h1 Manage Users 10 | 11 | .container(role='main') 12 | .row 13 | .col-md-6 14 | h2 Create User 15 | form.form-horizontal(action='/admin/user/create', method="POST") 16 | .mb-3 17 | .input-group 18 | label(for='usrName', class='sr-only') Username 19 | input.form-control(id="usrName", type="text", name="username", placeholder="Username", autofocus) 20 | i.input-group-text.fa.fa-user 21 | .mb-3 22 | .input-group 23 | label(for='passWrd', class='sr-only') Password 24 | input.form-control(id="passWrd", type="password", name="password", placeholder="Password") 25 | i.input-group-text.fa.fa-lock 26 | 27 | .mb-3 28 | .input-group 29 | label(for='email', class='sr-only') Password 30 | input.form-control(id="email", type="email", name="email", placeholder="Email") 31 | i.input-group-text @ 32 | 33 | button.btn.btn-primary.btn-lg(type="submit") Submit -------------------------------------------------------------------------------- /views/error.dt: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block title 4 | title Error 5 | 6 | block body 7 | div.container(role='main') 8 | .jumbotron 9 | h1 #{error.message} 10 | p.lead A #{error.code} error has occurred: 11 | //- p path: #{req.path} 12 | 13 | p 14 | a.btn.btn-primary(href="/") Return to the home page 15 | 16 | //- This bit should be removed in production environment 17 | -if(error.debugMessage.length) 18 | p The server produced the following stack trace: 19 | pre= error.debugMessage -------------------------------------------------------------------------------- /views/header.dt: -------------------------------------------------------------------------------- 1 | header 2 | #navbar-header.navbar-collapse.inverse.collapse(aria-expanded="false") 3 | .container 4 | - if (user.authenticated) 5 | a.nav-link(href="/logout") Logout 6 | 7 | nav.navbar.navbar-expand-md.navbar-dark.sticky-top.bg-dark(role='navigation') 8 | .container 9 | a.navbar-brand(href="/") 10 | | Eloquent 11 | ul.nav.navbar-nav.me-auto 12 | li.nav-item 13 | a.nav-link(href="/") Home 14 | - if (user.administrator) 15 | li.nav-item 16 | a.nav-link(href="/admin") Admin 17 | ul.nav.navbar-nav.pull-right 18 | - if (user.authenticated) 19 | li.nav-item 20 | a.nav-link(href="/profile/#{user.username}") 21 | i.fa.fa-user.mx-1 22 | | Profile 23 | li.nav-item 24 | a.nav-link(href="/logout") Logout 25 | button.navbar-toggler.collapsed(type="button", data-toggle="collapse", data-target="#navbar-header", aria-expanded="false") ☰ 26 | - else 27 | li.nav-item 28 | a.nav-link(href="/login") Login 29 | -------------------------------------------------------------------------------- /views/index.dt: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block title 4 | title Home 5 | 6 | block body 7 | .mb-4.bg-primary.bg-shadow 8 | .container.py-5 9 | h1& home.welcome-message 10 | 11 | .container(role='main') 12 | .row 13 | .col-md-12 14 | - foreach( p; blogPosts ) 15 | h2 #{p.title} 16 | p.lead Author #{p.author.displayname} (#{p.author.username}) 17 | p 18 | span.fa.fa-clock-o 19 | | 20 | | #{p.created} 21 | hr 22 | p !{p.content} 23 | a.btn.btn-primary(href="#")& txt.read-more 24 | | 25 | span.fa.fa-chevron-right 26 | hr -------------------------------------------------------------------------------- /views/layout.dt: -------------------------------------------------------------------------------- 1 | - void css(string file) 2 | link(rel='stylesheet', type='text/css', href='#{file}') 3 | 4 | doctype 5 5 | html(lang='en') 6 | head 7 | - auto versionNo = req.params["version"]; 8 | 9 | meta(charset='utf-8') 10 | meta(http-equiv='X-UA-Compatible', content='IE=edge') 11 | meta(name='viewport', content='width=device-width, initial-scale=1.0, shrink-to-fit=no') 12 | 13 | meta(name='application-name', content='Eloquent') 14 | meta(name='version', content='#{versionNo}') 15 | meta(name='description', content='') 16 | meta(name='author', content='') 17 | meta(name='keywords', content='') 18 | 19 | block title 20 | title Eloquent 21 | 22 | meta(name='msapplication-starturl', content='/') 23 | meta(name='msapplication-navbutton-color', content='#563d7c') 24 | meta(name='msapplication-tooltip', content='Open Eloquent in IE') 25 | meta(name='msapplication-TileImage', content='/images/logo144.png') 26 | meta(name='msapplication-TileColor', content='#563d7c') 27 | 28 | - css("https://fonts.googleapis.com/css?family=Open+Sans"); 29 | - css("/css/font-awesome.min.css"); 30 | - css("/css/bootstrap.min.css"); 31 | - css("/styles/style.css"); 32 | 33 | script(src='/js/popper.min.js') 34 | script(src='/js/bootstrap.min.js') 35 | 36 | body(role='document') 37 | include header 38 | 39 | section 40 | block body 41 | 42 | footer 43 | div.container 44 | hr(style='margin: 30px 0 10px 0;') 45 | p ©  46 | a(href='#') Samael Bate 47 | | 48 | | 2023 49 | -------------------------------------------------------------------------------- /views/login.dt: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block title 4 | title Login 5 | 6 | block body 7 | div.container(role='main') 8 | .row.justify-content-md-center.login-row 9 | .col-md-6.col-sm-8 10 | - if (user.authenticated) 11 | h4 Sign Out 12 | hr 13 | form.form-horizontal(action="logout", method="GET") 14 | button.btn.btn-primary.btn-lg.btn-block(type="submit") Log out 15 | - else 16 | h4 Log in 17 | hr 18 | form.form-horizontal(action="login", method="POST") 19 | .mb-3 20 | .input-group 21 | i.input-group-text.fa.fa-user 22 | input.form-control(id="usrName", type="text", name="username", placeholder="Username", autofocus) 23 | label(for='usrName', class='sr-only') Username 24 | .mb-3 25 | .input-group 26 | i.input-group-text.fa.fa-lock 27 | input.form-control(id="passWrd", type="password", name="password", placeholder="Password") 28 | label(for='passWrd', class='sr-only') Password 29 | 30 | button.btn.btn-primary.btn-lg(type="submit") Login 31 | - if (_error) 32 | p.text-danger #{_error} -------------------------------------------------------------------------------- /views/profile.dt: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block body 4 | div.container(role='main') 5 | .row.justify-content-md-center 6 | .col-md-6.col-sm-8 7 | - if (user.authenticated) 8 | .alert.alert-dismissable.alert-success 9 | button.close(type="button", data-dismiss="alert") x 10 | | Here are your posts #{user.username} 11 | - else 12 | .alert.alert-dismissable.alert-success 13 | button.close(type="button", data-dismiss="alert") x 14 | | Displaying posts for #{username} 15 | .row 16 | .col-md-12 17 | - foreach( p; blogPosts ) 18 | h2 #{p.title} 19 | p.lead #{p.author.displayname} 20 | p 21 | span.fa.fa-clock-o 22 | | #{p.created} 23 | hr 24 | p !{p.content} 25 | a.btn.btn-primary(href="#")& txt.read-more 26 | span.fa.fa-chevron-right 27 | hr -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@popperjs/core@^2.11.7": 6 | version "2.11.8" 7 | resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" 8 | integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== 9 | 10 | bootstrap@^5.3.0: 11 | version "5.3.0" 12 | resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.0.tgz#0718a7cc29040ee8dbf1bd652b896f3436a87c29" 13 | integrity sha512-UnBV3E3v4STVNQdms6jSGO2CvOkjUMdDAVR2V5N4uCMdaIkaQjbcEAMqRimDHIs4uqBYzDAKCQwCB+97tJgHQw== 14 | 15 | font-awesome@4.7.0: 16 | version "4.7.0" 17 | resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133" 18 | integrity sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM= 19 | --------------------------------------------------------------------------------