├── .dex.yaml ├── .docker ├── blogdb.dockerfile ├── blogdb.yml ├── database.yml └── mojo.dockerfile ├── .gitignore ├── DB ├── bin │ └── create-classes ├── dist.ini ├── etc │ ├── schema-update-2021-12-23.sql │ ├── schema-update-2022-01-22.2.sql │ ├── schema-update-2022-01-22.sql │ └── schema.sql └── lib │ └── BlogDB │ ├── DB.pm │ └── DB │ ├── Result │ ├── AuthPassword.pm │ ├── Blog.pm │ ├── BlogAuthorMap.pm │ ├── BlogEntry.pm │ ├── BlogSetting.pm │ ├── BlogTagMap.pm │ ├── Message.pm │ ├── PasswordToken.pm │ ├── PendingBlog.pm │ ├── PendingBlogEntry.pm │ ├── PendingBlogSetting.pm │ ├── PendingBlogTagMap.pm │ ├── PendingTag.pm │ ├── Person.pm │ ├── PersonFollowBlogMap.pm │ ├── PersonFollowPersonMap.pm │ ├── PersonSetting.pm │ ├── Tag.pm │ └── TagVote.pm │ └── ResultSet │ ├── Blog.pm │ └── BlogEntry.pm ├── README.md ├── Vagrantfile ├── Web ├── .dex.yaml ├── blogdb.docker.yml ├── cpanfile ├── lib │ ├── BlogDB │ │ ├── Scanner.pm │ │ ├── Web.pm │ │ └── Web │ │ │ ├── Command │ │ │ └── scan_blogs.pm │ │ │ ├── Controller │ │ │ ├── Blog.pm │ │ │ ├── Feed.pm │ │ │ ├── Root.pm │ │ │ ├── Tags.pm │ │ │ └── User.pm │ │ │ ├── Plugin │ │ │ └── MinionTasks.pm │ │ │ └── Test.pm │ └── Test │ │ └── Mojo │ │ └── BlogDB.pm ├── script │ ├── blogdb_web │ └── dbc ├── t │ ├── 01_endpoints │ │ ├── 01_root │ │ │ ├── 01_register.t │ │ │ ├── 02_login.t │ │ │ ├── 03_logout.t │ │ │ ├── 04_forgot_password.t │ │ │ └── 05_reset_password.t │ │ ├── 02_tags │ │ │ ├── 01_suggest.t │ │ │ ├── 02_vote.t │ │ │ ├── 03_approve.t │ │ │ └── 04_delete.t │ │ └── 03_blog │ │ │ ├── 01_new_blog.t │ │ │ ├── 02_edit_new_blog │ │ │ ├── 01_as_user.t │ │ │ ├── 02_with_token.t │ │ │ ├── 03_with_can_manage_blogs.t │ │ │ ├── 04_no_anonymous_edit.t │ │ │ └── 05_no_alt_user_edit.t │ │ │ ├── 03_publish_new_blog │ │ │ ├── 01_with_can_manage_blogs.t │ │ │ ├── 02_disable_for_normal_user.t │ │ │ └── 03_disable_for_anon_user.t │ │ │ ├── 04_view_blog.t │ │ │ ├── 05_edit_blog.t │ │ │ ├── 06_follow_blog.t │ │ │ └── 07_comment_on_blog.t │ ├── 02_html │ │ ├── 01_index │ │ │ └── 00_exists.t │ │ ├── 02_register │ │ │ └── 00_exists.t │ │ └── 03_forgot │ │ │ ├── 00_exists.t │ │ │ └── 01_var_token │ │ │ └── 00_exists.t │ └── 03_workflows │ │ ├── 01_register │ │ ├── 01_basic.t │ │ ├── 02_bad_confirmation.t │ │ ├── 03_no_password.t │ │ ├── 04_short_password.t │ │ ├── 05_no_confirmation.t │ │ ├── 06_username_taken.t │ │ └── 07_email_taken.t │ │ └── 02_login │ │ ├── 01_basic.t │ │ ├── 02_no_account.t │ │ └── 03_wrong_password.t └── templates │ ├── default │ ├── _ │ │ ├── form │ │ │ └── input.tx │ │ └── layout.tx │ ├── blog │ │ ├── _comment.tx │ │ ├── edit.html.tx │ │ ├── item.html.tx │ │ └── new │ │ │ ├── index.html.tx │ │ │ └── item.html.tx │ ├── forgot.html.tx │ ├── index.html.tx │ ├── new │ │ └── index.html.tx │ ├── register.html.tx │ ├── reset.html.tx │ └── tags │ │ └── index.html.tx │ └── simple │ ├── _ │ ├── _blog_card.tx │ ├── _blog_sidecard.tx │ ├── _blog_sidecard_new.tx │ ├── _entry_card.tx │ ├── form │ │ └── input.tx │ └── layout.tx │ ├── _public │ └── css │ │ └── styles.css │ ├── about.html.tx │ ├── blog │ ├── _comment.tx │ ├── edit.html.tx │ ├── index.html.tx │ ├── item.html.tx │ └── new │ │ ├── edit.html.tx │ │ ├── index.html.tx │ │ └── populating.html.tx │ ├── feed │ └── index.html.tx │ ├── forgot.html.tx │ ├── index.html.tx │ ├── register.html.tx │ ├── reset.html.tx │ ├── tags │ └── index.html.tx │ └── user │ ├── index.html.tx │ ├── settings.html.tx │ └── settings │ ├── _navtabs.tx │ ├── email.html.tx │ ├── following.html.tx │ └── password.html.tx └── system ├── setup-debian.sh ├── systemd ├── blogdb.screenshot.service ├── blogdb.web.service └── blogdb.worker.service └── vagrant-post-install.sh /.dex.yaml: -------------------------------------------------------------------------------- 1 | - name: db 2 | desc: "Control Devel DB Only" 3 | children: 4 | - name: start 5 | desc: "Start devel db on localhost via docker." 6 | shell: 7 | - docker-compose --project-directory ./DB -f ./.docker/database.yml up 8 | - name: stop 9 | desc: "Stop devel db on localhost via docker." 10 | shell: 11 | - docker-compose --project-directory ./DB -f ./.docker/database.yml down 12 | - name: status 13 | desc: "Show status of devel db." 14 | shell: 15 | - docker-compose --project-directory ./DB -f ./.docker/database.yml ps 16 | - name: reset 17 | desc: "Wipe devel db data." 18 | shell: 19 | - docker-compose --project-directory ./DB -f ./.docker/database.yml down -v 20 | - name: shell 21 | desc: "Grab a shell to psql" 22 | shell: 23 | - docker exec -ti blogdb-db psql -U blogdb blogdb 24 | - name: build 25 | desc: "Build packages or containers." 26 | children: 27 | - name: mojo 28 | desc: "Build the mojolicious base container." 29 | shell: 30 | - docker build . -t symkat/mojo -f .docker/mojo.dockerfile 31 | - name: blogdb 32 | desc: "Build the blogdb app container." 33 | shell: 34 | - docker build . -t symkat/blogdb -f .docker/blogdb.dockerfile 35 | 36 | - name: blogdb 37 | desc: "Control a full BlogDB instance with Docker." 38 | children: 39 | - name: start 40 | desc: "Start full BlogDB application." 41 | shell: 42 | - docker-compose --project-directory . -f ./.docker/blogdb.yml up 43 | - name: stop 44 | desc: "stop full BlogDB application." 45 | shell: 46 | - docker-compose --project-directory . -f ./.docker/blogdb.yml down 47 | - name: status 48 | desc: "Show status of full BlogDB application." 49 | shell: 50 | - docker-compose --project-directory . -f ./.docker/blogdb.yml ps 51 | - name: reset 52 | desc: "Destroy all data for BlogDB." 53 | shell: 54 | - docker-compose --project-directory . -f ./.docker/blogdb.yml down -v 55 | - name: appdb 56 | desc: "Grab a shell to psql" 57 | shell: 58 | - docker exec -ti blogdb-database psql -U blogdb blogdb 59 | - name: miniondb 60 | desc: "Grab a shell to psql" 61 | shell: 62 | - docker exec -ti blogdb-miniondb psql -U minion minion -------------------------------------------------------------------------------- /.docker/blogdb.dockerfile: -------------------------------------------------------------------------------- 1 | FROM symkat/mojo:latest 2 | 3 | USER root 4 | 5 | ADD . /home/app/src 6 | RUN chown -R app:app /home/app/src; 7 | 8 | USER app 9 | 10 | RUN eval $(perl -Mlocal::lib); \ 11 | cd /home/app/src/DB; \ 12 | dzil build; \ 13 | cpanm BlogDB-DB-*.tar.gz ; \ 14 | cd /home/app/src/Web; \ 15 | cpanm --installdeps .; \ 16 | cpanm --installdeps .; \ 17 | cpanm --installdeps .; 18 | -------------------------------------------------------------------------------- /.docker/blogdb.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | webserver: 5 | image: symkat/blogdb:latest 6 | ports: 7 | - 80:3000 8 | container_name: blogdb-webserver 9 | environment: 10 | # Configuration for perl 11 | PERL5LIB: '/home/app/perl5/lib/perl5' 12 | PERL_MB_OPT: '--install_base "/home/app/perl5"' 13 | PERL_MM_OPT: 'INSTALL_BASE=/home/app/perl5' 14 | PERL_LOCAL_LIB_ROOT: '/home/app/perl5' 15 | # Configuration For The App 16 | BDB_CONFIG_FILE: 'blogdb.docker.yml' 17 | WC_SCREENSHOT_URL: 'http://screenshot:3000' 18 | BDB_MINION_DB: 'postgresql://minion:minion@miniondb/minion' 19 | BDB_DEFAULT_DB: 'postgresql://blogdb:blogdb@database/blogdb' 20 | BDB_DOCKER_HOST_WAIT: 1, 21 | BDB_DHW_MINION_HOST: 'miniondb' 22 | BDB_DHW_DEFAULT_HOST: 'database' 23 | 24 | volumes: 25 | - ./Web:/home/app/BlogDB 26 | - blogdb_staticfs:/home/app/BlogDB/public 27 | working_dir: /home/app/BlogDB 28 | command: ./script/blogdb_web daemon 29 | depends_on: 30 | - "database" 31 | - "miniondb" 32 | worker: 33 | image: symkat/blogdb:latest 34 | container_name: blogdb-minion-worker 35 | environment: 36 | # Configuration for perl 37 | PERL5LIB: '/home/app/perl5/lib/perl5' 38 | PERL_MB_OPT: '--install_base "/home/app/perl5"' 39 | PERL_MM_OPT: 'INSTALL_BASE=/home/app/perl5' 40 | PERL_LOCAL_LIB_ROOT: '/home/app/perl5' 41 | # Configuration For The App 42 | BDB_CONFIG_FILE: 'blogdb.docker.yml' 43 | WC_SCREENSHOT_URL: 'http://screenshot:3000' 44 | BDB_MINION_DB: 'postgresql://minion:minion@miniondb/minion' 45 | BDB_DEFAULT_DB: 'postgresql://blogdb:blogdb@database/blogdb' 46 | BDB_DOCKER_HOST_WAIT: 1, 47 | BDB_DHW_MINION_HOST: 'miniondb' 48 | BDB_DHW_DEFAULT_HOST: 'database' 49 | volumes: 50 | - ./Web:/home/app/BlogDB 51 | - blogdb_staticfs:/home/app/BlogDB/public 52 | working_dir: /home/app/BlogDB 53 | command: ./script/blogdb_web minion worker 54 | depends_on: 55 | - "database" 56 | - "miniondb" 57 | database: 58 | image: postgres:11 59 | container_name: blogdb-database 60 | environment: 61 | - POSTGRES_PASSWORD=blogdb 62 | - POSTGRES_USER=blogdb 63 | - POSTGRES_DB=blogdb 64 | volumes: 65 | - ./DB/etc/schema.sql:/docker-entrypoint-initdb.d/000_schema.sql:ro 66 | - blogdb_database:/var/lib/postgresql/data 67 | miniondb: 68 | image: postgres:11 69 | container_name: blogdb-miniondb 70 | environment: 71 | - POSTGRES_PASSWORD=minion 72 | - POSTGRES_USER=minion 73 | - POSTGRES_DB=minion 74 | volumes: 75 | - minion_database:/var/lib/postgresql/data 76 | screenshot: 77 | image: elestio/ws-screenshot.slim 78 | container_name: blogdb-screenshot 79 | 80 | 81 | volumes: 82 | blogdb_database: 83 | blogdb_staticfs: 84 | minion_database: -------------------------------------------------------------------------------- /.docker/database.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | database: 5 | image: postgres:11 6 | container_name: blogdb-db 7 | ports: 8 | - 127.0.0.1:5432:5432 9 | environment: 10 | - POSTGRES_PASSWORD=blogdb 11 | - POSTGRES_USER=blogdb 12 | - POSTGRES_DB=blogdb 13 | volumes: 14 | - ./etc/schema.sql:/docker-entrypoint-initdb.d/000_schema.sql:ro 15 | - blogdb_database:/var/lib/postgresql/data 16 | minion-database: 17 | image: postgres:11 18 | container_name: blogdb-minion 19 | ports: 20 | - 127.0.0.1:5400:5432 21 | environment: 22 | - POSTGRES_PASSWORD=minion 23 | - POSTGRES_USER=minion 24 | - POSTGRES_DB=minion 25 | volumes: 26 | - minion_database:/var/lib/postgresql/data 27 | 28 | 29 | volumes: 30 | blogdb_database: 31 | minion_database: 32 | -------------------------------------------------------------------------------- /.docker/mojo.dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:11 2 | 3 | RUN apt-get update; \ 4 | apt-get install -y git build-essential libpq-dev libssl-dev libz-dev libexpat1-dev cpanminus liblocal-lib-perl \ 5 | postgresql-client postgresql-contrib postgresql python3-psycopg2; \ 6 | useradd -U -s /bin/bash -m app; 7 | 8 | USER app 9 | RUN eval $(perl -Mlocal::lib); \ 10 | echo 'eval $(perl -Mlocal::lib)' >> /home/app/.bashrc; \ 11 | cpanm Dist::Zilla Archive::Zip Minion Mojolicious::Plugin::XslateRenderer Mojolicious::Plugin::RenderFile Mojo::Pg \ 12 | DateTime::Format::Pg WebService::WsScreenshot LWP::UserAgent XML::RSS Test::Postgresql58 Test::More Test::Deep \ 13 | DBIx::Class::InflateColumn::Serializer DBIx::Class::Schema::Config DBIx::Class::DeploymentHandler Data::GUID \ 14 | MooseX::AttributeShortcuts MooseX::Getopt DBD::Pg; \ 15 | cpanm Dist::Zilla Archive::Zip Minion Mojolicious::Plugin::XslateRenderer Mojolicious::Plugin::RenderFile Mojo::Pg \ 16 | DateTime::Format::Pg WebService::WsScreenshot LWP::UserAgent XML::RSS Test::Postgresql58 Test::More Test::Deep \ 17 | DBIx::Class::InflateColumn::Serializer DBIx::Class::Schema::Config DBIx::Class::DeploymentHandler Data::GUID \ 18 | MooseX::AttributeShortcuts MooseX::Getopt DBD::Pg; \ 19 | cpanm Dist::Zilla Archive::Zip Minion Mojolicious::Plugin::XslateRenderer Mojolicious::Plugin::RenderFile Mojo::Pg \ 20 | DateTime::Format::Pg WebService::WsScreenshot LWP::UserAgent XML::RSS Test::Postgresql58 Test::More Test::Deep \ 21 | DBIx::Class::InflateColumn::Serializer DBIx::Class::Schema::Config DBIx::Class::DeploymentHandler Data::GUID \ 22 | MooseX::AttributeShortcuts MooseX::Getopt DBD::Pg; \ 23 | cpanm Dist::Zilla Archive::Zip Minion Mojolicious::Plugin::XslateRenderer Mojolicious::Plugin::RenderFile Mojo::Pg \ 24 | DateTime::Format::Pg WebService::WsScreenshot LWP::UserAgent XML::RSS Test::Postgresql58 Test::More Test::Deep \ 25 | DBIx::Class::InflateColumn::Serializer DBIx::Class::Schema::Config DBIx::Class::DeploymentHandler Data::GUID \ 26 | MooseX::AttributeShortcuts MooseX::Getopt DBD::Pg; \ 27 | cpanm Dist::Zilla Archive::Zip Minion Mojolicious::Plugin::XslateRenderer Mojolicious::Plugin::RenderFile Mojo::Pg \ 28 | DateTime::Format::Pg WebService::WsScreenshot LWP::UserAgent XML::RSS Test::Postgresql58 Test::More Test::Deep \ 29 | DBIx::Class::InflateColumn::Serializer DBIx::Class::Schema::Config DBIx::Class::DeploymentHandler Data::GUID \ 30 | MooseX::AttributeShortcuts MooseX::Getopt DBD::Pg; 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | Web/local 3 | Web/devel 4 | Web/pans 5 | Web/tmp 6 | Web/.plx 7 | Web/cpanfile.snapshot 8 | Web/blogdb.yml 9 | Web/public/screenshots/ 10 | DB/BlogDB-DB-* 11 | -------------------------------------------------------------------------------- /DB/bin/create-classes: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CLASS_NAME="BlogDB::DB" 4 | 5 | # Generate a random 8 character name for the docker container that holds the PSQL 6 | # database. 7 | PSQL_NAME=$(cat /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z' | fold -w 8 | head -n 1) 8 | 9 | # Launch a PSQL Instance 10 | PSQL_DOCKER=`docker run --rm --name $PSQL_NAME -e POSTGRES_PASSWORD=dbic -e POSTGRES_USER=dbic -e POSTGRES_DB=dbic -d \ 11 | --mount type=bind,src=$PWD/etc/schema.sql,dst=/docker-entrypoint-initdb.d/schema.sql postgres:11` 12 | 13 | docker run --rm --link $PSQL_NAME:psqldb --mount type=bind,src=$PWD,dst=/app symkat/schema_builder /bin/build-schema $CLASS_NAME 14 | 15 | docker kill $PSQL_DOCKER 16 | 17 | sudo chown -R $USER:$USER lib 18 | -------------------------------------------------------------------------------- /DB/dist.ini: -------------------------------------------------------------------------------- 1 | name = BlogDB-DB 2 | author = Kaitlyn Parkhurst 3 | license = Perl_5 4 | copyright_holder = Kaitlyn Parkhurst 5 | copyright_year = 2021 6 | abstract = BlogDB's Database 7 | version = 1 8 | 9 | [@Basic] 10 | 11 | [Prereqs] 12 | DBIx::Class::InflateColumn::Serializer = 0 13 | DBIx::Class::Schema::Config = 0 14 | DBIx::Class::Schema::ResultSetNames = 0 15 | DBIx::Class::DeploymentHandler = 0 16 | MooseX::AttributeShortcuts = 0 17 | MooseX::Getopt = 0 18 | Data::GUID = 0 19 | DBD::Pg = 0 20 | 21 | [AutoPrereqs] 22 | -------------------------------------------------------------------------------- /DB/etc/schema-update-2021-12-23.sql: -------------------------------------------------------------------------------- 1 | 2 | -- Settings for a given blog. 3 | create TABLE blog_settings ( 4 | id serial PRIMARY KEY, 5 | blog_id int not null references blog(id), 6 | name text not null, 7 | value json not null default '{}', 8 | created_at timestamptz not null default current_timestamp, 9 | 10 | -- Allow ->find_or_new_related() 11 | CONSTRAINT unq_blog_id_name UNIQUE(blog_id, name) 12 | ); 13 | 14 | 15 | -- Settings for a given pending_blog. 16 | create TABLE pending_blog_settings ( 17 | id serial PRIMARY KEY, 18 | pending_blog_id int not null references pending_blog(id), 19 | name text not null, 20 | value json not null default '{}', 21 | created_at timestamptz not null default current_timestamp, 22 | 23 | -- Allow ->find_or_new_related() 24 | CONSTRAINT unq_pending_blog_id_name UNIQUE(pending_blog_id, name) 25 | ); -------------------------------------------------------------------------------- /DB/etc/schema-update-2022-01-22.2.sql: -------------------------------------------------------------------------------- 1 | 2 | -- Allow the follow statuses to be public or private. 3 | ALTER TABLE person_follow_blog_map 4 | ADD COLUMN is_public boolean not null default false; 5 | 6 | ALTER TABLE person_follow_person_map 7 | ADD COLUMN is_public boolean not null default false; -------------------------------------------------------------------------------- /DB/etc/schema-update-2022-01-22.sql: -------------------------------------------------------------------------------- 1 | 2 | -- Allow blog authors, such that a blog listing may say "written by Alice, Mallory & Bob", 3 | -- and each of their profiles may list the blog as something they write. 4 | CREATE TABLE blog_author_map ( 5 | id serial PRIMARY KEY, 6 | blog_id int not null references blog(id), 7 | person_id int not null references person(id), 8 | created_at timestamptz not null default current_timestamp 9 | ); 10 | -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package BlogDB::DB; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | use strict; 8 | use warnings; 9 | 10 | use base 'DBIx::Class::Schema'; 11 | 12 | __PACKAGE__->load_components("Schema::Config", "Schema::ResultSetNames"); 13 | 14 | __PACKAGE__->load_namespaces; 15 | 16 | 17 | # Created by DBIx::Class::Schema::Loader v0.07049 @ 2022-02-01 01:10:27 18 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:WJYkMkUHcfjCAq1mZSDZ9A 19 | 20 | our $VERSION = 1; 21 | 22 | sub install_defaults { 23 | my ( $self ) = @_; 24 | } 25 | 26 | 1; 27 | -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB/Result/AuthPassword.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package BlogDB::DB::Result::AuthPassword; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | =head1 NAME 8 | 9 | BlogDB::DB::Result::AuthPassword 10 | 11 | =cut 12 | 13 | use strict; 14 | use warnings; 15 | 16 | use base 'DBIx::Class::Core'; 17 | 18 | =head1 COMPONENTS LOADED 19 | 20 | =over 4 21 | 22 | =item * L 23 | 24 | =item * L 25 | 26 | =back 27 | 28 | =cut 29 | 30 | __PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); 31 | 32 | =head1 TABLE: C 33 | 34 | =cut 35 | 36 | __PACKAGE__->table("auth_password"); 37 | 38 | =head1 ACCESSORS 39 | 40 | =head2 person_id 41 | 42 | data_type: 'integer' 43 | is_foreign_key: 1 44 | is_nullable: 0 45 | 46 | =head2 password 47 | 48 | data_type: 'text' 49 | is_nullable: 0 50 | 51 | =head2 salt 52 | 53 | data_type: 'text' 54 | is_nullable: 0 55 | 56 | =head2 updated_at 57 | 58 | data_type: 'timestamp with time zone' 59 | default_value: current_timestamp 60 | is_nullable: 0 61 | 62 | =head2 created_at 63 | 64 | data_type: 'timestamp with time zone' 65 | default_value: current_timestamp 66 | is_nullable: 0 67 | 68 | =cut 69 | 70 | __PACKAGE__->add_columns( 71 | "person_id", 72 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 73 | "password", 74 | { data_type => "text", is_nullable => 0 }, 75 | "salt", 76 | { data_type => "text", is_nullable => 0 }, 77 | "updated_at", 78 | { 79 | data_type => "timestamp with time zone", 80 | default_value => \"current_timestamp", 81 | is_nullable => 0, 82 | }, 83 | "created_at", 84 | { 85 | data_type => "timestamp with time zone", 86 | default_value => \"current_timestamp", 87 | is_nullable => 0, 88 | }, 89 | ); 90 | 91 | =head1 UNIQUE CONSTRAINTS 92 | 93 | =head2 C 94 | 95 | =over 4 96 | 97 | =item * L 98 | 99 | =back 100 | 101 | =cut 102 | 103 | __PACKAGE__->add_unique_constraint("auth_password_person_id_key", ["person_id"]); 104 | 105 | =head1 RELATIONS 106 | 107 | =head2 person 108 | 109 | Type: belongs_to 110 | 111 | Related object: L 112 | 113 | =cut 114 | 115 | __PACKAGE__->belongs_to( 116 | "person", 117 | "BlogDB::DB::Result::Person", 118 | { id => "person_id" }, 119 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 120 | ); 121 | 122 | 123 | # Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-10-06 18:19:48 124 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:baAKlOZh2bG24sTat3UgSw 125 | 126 | __PACKAGE__->set_primary_key('person_id'); 127 | 128 | use Crypt::Eksblowfish::Bcrypt qw( bcrypt_hash en_base64 de_base64 ); 129 | use Crypt::Random; 130 | 131 | sub check_password { 132 | my ( $self, $password ) = @_; 133 | return de_base64($self->password) eq bcrypt_hash({ 134 | key_nul => 1, 135 | cost => 8, 136 | salt => de_base64($self->salt), 137 | }, $password ); 138 | } 139 | 140 | sub set_password { 141 | my ( $self, $password ) = @_; 142 | $self->_fill_password( $password ); 143 | $self->insert; 144 | return $self; 145 | } 146 | 147 | sub update_password { 148 | my ( $self, $password ) = @_; 149 | $self->_fill_password( $password ); 150 | $self->update; 151 | return $self; 152 | } 153 | 154 | sub _fill_password { 155 | my ( $self, $password ) = @_; 156 | 157 | my $salt = random_salt(); 158 | 159 | $self->password( 160 | en_base64( 161 | bcrypt_hash({ 162 | key_nul => 1, 163 | cost => 8, 164 | salt => $salt, 165 | }, $password ) 166 | ) 167 | ); 168 | 169 | $self->salt( en_base64($salt) ); 170 | } 171 | 172 | sub random_salt { 173 | Crypt::Random::makerandom_octet( Length => 16 ); 174 | } 175 | 176 | 1; 177 | -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB/Result/BlogAuthorMap.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package BlogDB::DB::Result::BlogAuthorMap; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | =head1 NAME 8 | 9 | BlogDB::DB::Result::BlogAuthorMap 10 | 11 | =cut 12 | 13 | use strict; 14 | use warnings; 15 | 16 | use base 'DBIx::Class::Core'; 17 | 18 | =head1 COMPONENTS LOADED 19 | 20 | =over 4 21 | 22 | =item * L 23 | 24 | =item * L 25 | 26 | =back 27 | 28 | =cut 29 | 30 | __PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); 31 | 32 | =head1 TABLE: C 33 | 34 | =cut 35 | 36 | __PACKAGE__->table("blog_author_map"); 37 | 38 | =head1 ACCESSORS 39 | 40 | =head2 id 41 | 42 | data_type: 'integer' 43 | is_auto_increment: 1 44 | is_nullable: 0 45 | sequence: 'blog_author_map_id_seq' 46 | 47 | =head2 blog_id 48 | 49 | data_type: 'integer' 50 | is_foreign_key: 1 51 | is_nullable: 0 52 | 53 | =head2 person_id 54 | 55 | data_type: 'integer' 56 | is_foreign_key: 1 57 | is_nullable: 0 58 | 59 | =head2 created_at 60 | 61 | data_type: 'timestamp with time zone' 62 | default_value: current_timestamp 63 | is_nullable: 0 64 | 65 | =cut 66 | 67 | __PACKAGE__->add_columns( 68 | "id", 69 | { 70 | data_type => "integer", 71 | is_auto_increment => 1, 72 | is_nullable => 0, 73 | sequence => "blog_author_map_id_seq", 74 | }, 75 | "blog_id", 76 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 77 | "person_id", 78 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 79 | "created_at", 80 | { 81 | data_type => "timestamp with time zone", 82 | default_value => \"current_timestamp", 83 | is_nullable => 0, 84 | }, 85 | ); 86 | 87 | =head1 PRIMARY KEY 88 | 89 | =over 4 90 | 91 | =item * L 92 | 93 | =back 94 | 95 | =cut 96 | 97 | __PACKAGE__->set_primary_key("id"); 98 | 99 | =head1 RELATIONS 100 | 101 | =head2 blog 102 | 103 | Type: belongs_to 104 | 105 | Related object: L 106 | 107 | =cut 108 | 109 | __PACKAGE__->belongs_to( 110 | "blog", 111 | "BlogDB::DB::Result::Blog", 112 | { id => "blog_id" }, 113 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 114 | ); 115 | 116 | =head2 person 117 | 118 | Type: belongs_to 119 | 120 | Related object: L 121 | 122 | =cut 123 | 124 | __PACKAGE__->belongs_to( 125 | "person", 126 | "BlogDB::DB::Result::Person", 127 | { id => "person_id" }, 128 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 129 | ); 130 | 131 | 132 | # Created by DBIx::Class::Schema::Loader v0.07049 @ 2022-01-22 15:10:12 133 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:m34MXvflJFdiA6y/4ADTHw 134 | 135 | 136 | # You can replace this text with custom code or comments, and it will be preserved on regeneration 137 | 1; 138 | -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB/Result/BlogEntry.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package BlogDB::DB::Result::BlogEntry; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | =head1 NAME 8 | 9 | BlogDB::DB::Result::BlogEntry 10 | 11 | =cut 12 | 13 | use strict; 14 | use warnings; 15 | 16 | use base 'DBIx::Class::Core'; 17 | 18 | =head1 COMPONENTS LOADED 19 | 20 | =over 4 21 | 22 | =item * L 23 | 24 | =item * L 25 | 26 | =back 27 | 28 | =cut 29 | 30 | __PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); 31 | 32 | =head1 TABLE: C 33 | 34 | =cut 35 | 36 | __PACKAGE__->table("blog_entry"); 37 | 38 | =head1 ACCESSORS 39 | 40 | =head2 id 41 | 42 | data_type: 'integer' 43 | is_auto_increment: 1 44 | is_nullable: 0 45 | sequence: 'blog_entry_id_seq' 46 | 47 | =head2 blog_id 48 | 49 | data_type: 'integer' 50 | is_foreign_key: 1 51 | is_nullable: 0 52 | 53 | =head2 title 54 | 55 | data_type: 'text' 56 | is_nullable: 0 57 | 58 | =head2 url 59 | 60 | data_type: 'text' 61 | is_nullable: 0 62 | 63 | =head2 publish_date 64 | 65 | data_type: 'timestamp with time zone' 66 | is_nullable: 0 67 | 68 | =head2 description 69 | 70 | data_type: 'text' 71 | is_nullable: 1 72 | 73 | =head2 created_at 74 | 75 | data_type: 'timestamp with time zone' 76 | default_value: current_timestamp 77 | is_nullable: 0 78 | 79 | =cut 80 | 81 | __PACKAGE__->add_columns( 82 | "id", 83 | { 84 | data_type => "integer", 85 | is_auto_increment => 1, 86 | is_nullable => 0, 87 | sequence => "blog_entry_id_seq", 88 | }, 89 | "blog_id", 90 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 91 | "title", 92 | { data_type => "text", is_nullable => 0 }, 93 | "url", 94 | { data_type => "text", is_nullable => 0 }, 95 | "publish_date", 96 | { data_type => "timestamp with time zone", is_nullable => 0 }, 97 | "description", 98 | { data_type => "text", is_nullable => 1 }, 99 | "created_at", 100 | { 101 | data_type => "timestamp with time zone", 102 | default_value => \"current_timestamp", 103 | is_nullable => 0, 104 | }, 105 | ); 106 | 107 | =head1 PRIMARY KEY 108 | 109 | =over 4 110 | 111 | =item * L 112 | 113 | =back 114 | 115 | =cut 116 | 117 | __PACKAGE__->set_primary_key("id"); 118 | 119 | =head1 RELATIONS 120 | 121 | =head2 blog 122 | 123 | Type: belongs_to 124 | 125 | Related object: L 126 | 127 | =cut 128 | 129 | __PACKAGE__->belongs_to( 130 | "blog", 131 | "BlogDB::DB::Result::Blog", 132 | { id => "blog_id" }, 133 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 134 | ); 135 | 136 | 137 | # Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-10-06 18:19:48 138 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:kxnEMUyBOfQ0qkeWO/qSMg 139 | 140 | sub published_ago { 141 | my ( $self ) = @_; 142 | 143 | my $delta = DateTime->now->delta_days( $self->publish_date ); 144 | 145 | my $days = $delta->in_units('days'); 146 | 147 | return 'today' if $days == 0; 148 | return 'yesterday' if $days == 1; 149 | return "$days days ago" if $days < 10; 150 | return int( $days / 7 ) . " weeks ago" if $days < 60; 151 | return int( $days / 30 ) . " months ago" if $days < 365; 152 | return "last year" if $days < 730; 153 | return int( $days / 365 ) . " years ago"; 154 | } 155 | 156 | # You can replace this text with custom code or comments, and it will be preserved on regeneration 157 | 1; 158 | -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB/Result/BlogSetting.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package BlogDB::DB::Result::BlogSetting; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | =head1 NAME 8 | 9 | BlogDB::DB::Result::BlogSetting 10 | 11 | =cut 12 | 13 | use strict; 14 | use warnings; 15 | 16 | use base 'DBIx::Class::Core'; 17 | 18 | =head1 COMPONENTS LOADED 19 | 20 | =over 4 21 | 22 | =item * L 23 | 24 | =item * L 25 | 26 | =back 27 | 28 | =cut 29 | 30 | __PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); 31 | 32 | =head1 TABLE: C 33 | 34 | =cut 35 | 36 | __PACKAGE__->table("blog_settings"); 37 | 38 | =head1 ACCESSORS 39 | 40 | =head2 id 41 | 42 | data_type: 'integer' 43 | is_auto_increment: 1 44 | is_nullable: 0 45 | sequence: 'blog_settings_id_seq' 46 | 47 | =head2 blog_id 48 | 49 | data_type: 'integer' 50 | is_foreign_key: 1 51 | is_nullable: 0 52 | 53 | =head2 name 54 | 55 | data_type: 'text' 56 | is_nullable: 0 57 | 58 | =head2 value 59 | 60 | data_type: 'json' 61 | default_value: '{}' 62 | is_nullable: 0 63 | serializer_class: 'JSON' 64 | 65 | =head2 created_at 66 | 67 | data_type: 'timestamp with time zone' 68 | default_value: current_timestamp 69 | is_nullable: 0 70 | 71 | =cut 72 | 73 | __PACKAGE__->add_columns( 74 | "id", 75 | { 76 | data_type => "integer", 77 | is_auto_increment => 1, 78 | is_nullable => 0, 79 | sequence => "blog_settings_id_seq", 80 | }, 81 | "blog_id", 82 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 83 | "name", 84 | { data_type => "text", is_nullable => 0 }, 85 | "value", 86 | { 87 | data_type => "json", 88 | default_value => "{}", 89 | is_nullable => 0, 90 | serializer_class => "JSON", 91 | }, 92 | "created_at", 93 | { 94 | data_type => "timestamp with time zone", 95 | default_value => \"current_timestamp", 96 | is_nullable => 0, 97 | }, 98 | ); 99 | 100 | =head1 PRIMARY KEY 101 | 102 | =over 4 103 | 104 | =item * L 105 | 106 | =back 107 | 108 | =cut 109 | 110 | __PACKAGE__->set_primary_key("id"); 111 | 112 | =head1 UNIQUE CONSTRAINTS 113 | 114 | =head2 C 115 | 116 | =over 4 117 | 118 | =item * L 119 | 120 | =item * L 121 | 122 | =back 123 | 124 | =cut 125 | 126 | __PACKAGE__->add_unique_constraint("unq_blog_id_name", ["blog_id", "name"]); 127 | 128 | =head1 RELATIONS 129 | 130 | =head2 blog 131 | 132 | Type: belongs_to 133 | 134 | Related object: L 135 | 136 | =cut 137 | 138 | __PACKAGE__->belongs_to( 139 | "blog", 140 | "BlogDB::DB::Result::Blog", 141 | { id => "blog_id" }, 142 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 143 | ); 144 | 145 | 146 | # Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-12-27 00:20:15 147 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Ug23P3Xp4EGXbwg++D4ssw 148 | 149 | 150 | # You can replace this text with custom code or comments, and it will be preserved on regeneration 151 | 1; 152 | -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB/Result/BlogTagMap.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package BlogDB::DB::Result::BlogTagMap; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | =head1 NAME 8 | 9 | BlogDB::DB::Result::BlogTagMap 10 | 11 | =cut 12 | 13 | use strict; 14 | use warnings; 15 | 16 | use base 'DBIx::Class::Core'; 17 | 18 | =head1 COMPONENTS LOADED 19 | 20 | =over 4 21 | 22 | =item * L 23 | 24 | =item * L 25 | 26 | =back 27 | 28 | =cut 29 | 30 | __PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); 31 | 32 | =head1 TABLE: C 33 | 34 | =cut 35 | 36 | __PACKAGE__->table("blog_tag_map"); 37 | 38 | =head1 ACCESSORS 39 | 40 | =head2 id 41 | 42 | data_type: 'integer' 43 | is_auto_increment: 1 44 | is_nullable: 0 45 | sequence: 'blog_tag_map_id_seq' 46 | 47 | =head2 blog_id 48 | 49 | data_type: 'integer' 50 | is_foreign_key: 1 51 | is_nullable: 0 52 | 53 | =head2 tag_id 54 | 55 | data_type: 'integer' 56 | is_foreign_key: 1 57 | is_nullable: 0 58 | 59 | =head2 created_at 60 | 61 | data_type: 'timestamp with time zone' 62 | default_value: current_timestamp 63 | is_nullable: 0 64 | 65 | =cut 66 | 67 | __PACKAGE__->add_columns( 68 | "id", 69 | { 70 | data_type => "integer", 71 | is_auto_increment => 1, 72 | is_nullable => 0, 73 | sequence => "blog_tag_map_id_seq", 74 | }, 75 | "blog_id", 76 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 77 | "tag_id", 78 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 79 | "created_at", 80 | { 81 | data_type => "timestamp with time zone", 82 | default_value => \"current_timestamp", 83 | is_nullable => 0, 84 | }, 85 | ); 86 | 87 | =head1 PRIMARY KEY 88 | 89 | =over 4 90 | 91 | =item * L 92 | 93 | =back 94 | 95 | =cut 96 | 97 | __PACKAGE__->set_primary_key("id"); 98 | 99 | =head1 RELATIONS 100 | 101 | =head2 blog 102 | 103 | Type: belongs_to 104 | 105 | Related object: L 106 | 107 | =cut 108 | 109 | __PACKAGE__->belongs_to( 110 | "blog", 111 | "BlogDB::DB::Result::Blog", 112 | { id => "blog_id" }, 113 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 114 | ); 115 | 116 | =head2 tag 117 | 118 | Type: belongs_to 119 | 120 | Related object: L 121 | 122 | =cut 123 | 124 | __PACKAGE__->belongs_to( 125 | "tag", 126 | "BlogDB::DB::Result::Tag", 127 | { id => "tag_id" }, 128 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 129 | ); 130 | 131 | 132 | # Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-10-06 18:19:48 133 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:t5kWJpbYkAFpKWVIW92e5A 134 | 135 | 136 | # You can replace this text with custom code or comments, and it will be preserved on regeneration 137 | 1; 138 | -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB/Result/PasswordToken.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package BlogDB::DB::Result::PasswordToken; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | =head1 NAME 8 | 9 | BlogDB::DB::Result::PasswordToken 10 | 11 | =cut 12 | 13 | use strict; 14 | use warnings; 15 | 16 | use base 'DBIx::Class::Core'; 17 | 18 | =head1 COMPONENTS LOADED 19 | 20 | =over 4 21 | 22 | =item * L 23 | 24 | =item * L 25 | 26 | =back 27 | 28 | =cut 29 | 30 | __PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); 31 | 32 | =head1 TABLE: C 33 | 34 | =cut 35 | 36 | __PACKAGE__->table("password_token"); 37 | 38 | =head1 ACCESSORS 39 | 40 | =head2 id 41 | 42 | data_type: 'integer' 43 | is_auto_increment: 1 44 | is_nullable: 0 45 | sequence: 'password_token_id_seq' 46 | 47 | =head2 person_id 48 | 49 | data_type: 'integer' 50 | is_foreign_key: 1 51 | is_nullable: 0 52 | 53 | =head2 token 54 | 55 | data_type: 'text' 56 | is_nullable: 0 57 | 58 | =head2 is_redeemed 59 | 60 | data_type: 'boolean' 61 | default_value: false 62 | is_nullable: 0 63 | 64 | =head2 created_at 65 | 66 | data_type: 'timestamp with time zone' 67 | default_value: current_timestamp 68 | is_nullable: 0 69 | 70 | =cut 71 | 72 | __PACKAGE__->add_columns( 73 | "id", 74 | { 75 | data_type => "integer", 76 | is_auto_increment => 1, 77 | is_nullable => 0, 78 | sequence => "password_token_id_seq", 79 | }, 80 | "person_id", 81 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 82 | "token", 83 | { data_type => "text", is_nullable => 0 }, 84 | "is_redeemed", 85 | { data_type => "boolean", default_value => \"false", is_nullable => 0 }, 86 | "created_at", 87 | { 88 | data_type => "timestamp with time zone", 89 | default_value => \"current_timestamp", 90 | is_nullable => 0, 91 | }, 92 | ); 93 | 94 | =head1 PRIMARY KEY 95 | 96 | =over 4 97 | 98 | =item * L 99 | 100 | =back 101 | 102 | =cut 103 | 104 | __PACKAGE__->set_primary_key("id"); 105 | 106 | =head1 RELATIONS 107 | 108 | =head2 person 109 | 110 | Type: belongs_to 111 | 112 | Related object: L 113 | 114 | =cut 115 | 116 | __PACKAGE__->belongs_to( 117 | "person", 118 | "BlogDB::DB::Result::Person", 119 | { id => "person_id" }, 120 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 121 | ); 122 | 123 | 124 | # Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-10-06 18:19:48 125 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:q7P5QtdsUuqq0Lg/rzRJ+Q 126 | 127 | 128 | # You can replace this text with custom code or comments, and it will be preserved on regeneration 129 | 1; 130 | -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB/Result/PendingBlogEntry.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package BlogDB::DB::Result::PendingBlogEntry; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | =head1 NAME 8 | 9 | BlogDB::DB::Result::PendingBlogEntry 10 | 11 | =cut 12 | 13 | use strict; 14 | use warnings; 15 | 16 | use base 'DBIx::Class::Core'; 17 | 18 | =head1 COMPONENTS LOADED 19 | 20 | =over 4 21 | 22 | =item * L 23 | 24 | =item * L 25 | 26 | =back 27 | 28 | =cut 29 | 30 | __PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); 31 | 32 | =head1 TABLE: C 33 | 34 | =cut 35 | 36 | __PACKAGE__->table("pending_blog_entry"); 37 | 38 | =head1 ACCESSORS 39 | 40 | =head2 id 41 | 42 | data_type: 'integer' 43 | is_auto_increment: 1 44 | is_nullable: 0 45 | sequence: 'pending_blog_entry_id_seq' 46 | 47 | =head2 blog_id 48 | 49 | data_type: 'integer' 50 | is_foreign_key: 1 51 | is_nullable: 0 52 | 53 | =head2 title 54 | 55 | data_type: 'text' 56 | is_nullable: 0 57 | 58 | =head2 url 59 | 60 | data_type: 'text' 61 | is_nullable: 0 62 | 63 | =head2 publish_date 64 | 65 | data_type: 'timestamp with time zone' 66 | is_nullable: 0 67 | 68 | =head2 description 69 | 70 | data_type: 'text' 71 | is_nullable: 1 72 | 73 | =head2 created_at 74 | 75 | data_type: 'timestamp with time zone' 76 | default_value: current_timestamp 77 | is_nullable: 0 78 | 79 | =cut 80 | 81 | __PACKAGE__->add_columns( 82 | "id", 83 | { 84 | data_type => "integer", 85 | is_auto_increment => 1, 86 | is_nullable => 0, 87 | sequence => "pending_blog_entry_id_seq", 88 | }, 89 | "blog_id", 90 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 91 | "title", 92 | { data_type => "text", is_nullable => 0 }, 93 | "url", 94 | { data_type => "text", is_nullable => 0 }, 95 | "publish_date", 96 | { data_type => "timestamp with time zone", is_nullable => 0 }, 97 | "description", 98 | { data_type => "text", is_nullable => 1 }, 99 | "created_at", 100 | { 101 | data_type => "timestamp with time zone", 102 | default_value => \"current_timestamp", 103 | is_nullable => 0, 104 | }, 105 | ); 106 | 107 | =head1 PRIMARY KEY 108 | 109 | =over 4 110 | 111 | =item * L 112 | 113 | =back 114 | 115 | =cut 116 | 117 | __PACKAGE__->set_primary_key("id"); 118 | 119 | =head1 RELATIONS 120 | 121 | =head2 blog 122 | 123 | Type: belongs_to 124 | 125 | Related object: L 126 | 127 | =cut 128 | 129 | __PACKAGE__->belongs_to( 130 | "blog", 131 | "BlogDB::DB::Result::PendingBlog", 132 | { id => "blog_id" }, 133 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 134 | ); 135 | 136 | 137 | # Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-11-24 04:45:56 138 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:n4ZdA/apfHAt4UlFa9ybSA 139 | 140 | 141 | # You can replace this text with custom code or comments, and it will be preserved on regeneration 142 | 143 | sub published_ago { 144 | my ( $self ) = @_; 145 | 146 | my $delta = DateTime->now->delta_days( $self->publish_date ); 147 | 148 | my $days = $delta->in_units('days'); 149 | 150 | return 'today' if $days == 0; 151 | return 'yesterday' if $days == 1; 152 | return "$days days ago" if $days < 10; 153 | return int( $days / 7 ) . " weeks ago" if $days < 60; 154 | return int( $days / 30 ) . " months ago" if $days < 365; 155 | return "last year" if $days < 730; 156 | return int( $days / 365 ) . " years ago"; 157 | } 158 | 159 | 1; 160 | -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB/Result/PendingBlogSetting.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package BlogDB::DB::Result::PendingBlogSetting; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | =head1 NAME 8 | 9 | BlogDB::DB::Result::PendingBlogSetting 10 | 11 | =cut 12 | 13 | use strict; 14 | use warnings; 15 | 16 | use base 'DBIx::Class::Core'; 17 | 18 | =head1 COMPONENTS LOADED 19 | 20 | =over 4 21 | 22 | =item * L 23 | 24 | =item * L 25 | 26 | =back 27 | 28 | =cut 29 | 30 | __PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); 31 | 32 | =head1 TABLE: C 33 | 34 | =cut 35 | 36 | __PACKAGE__->table("pending_blog_settings"); 37 | 38 | =head1 ACCESSORS 39 | 40 | =head2 id 41 | 42 | data_type: 'integer' 43 | is_auto_increment: 1 44 | is_nullable: 0 45 | sequence: 'pending_blog_settings_id_seq' 46 | 47 | =head2 pending_blog_id 48 | 49 | data_type: 'integer' 50 | is_foreign_key: 1 51 | is_nullable: 0 52 | 53 | =head2 name 54 | 55 | data_type: 'text' 56 | is_nullable: 0 57 | 58 | =head2 value 59 | 60 | data_type: 'json' 61 | default_value: '{}' 62 | is_nullable: 0 63 | serializer_class: 'JSON' 64 | 65 | =head2 created_at 66 | 67 | data_type: 'timestamp with time zone' 68 | default_value: current_timestamp 69 | is_nullable: 0 70 | 71 | =cut 72 | 73 | __PACKAGE__->add_columns( 74 | "id", 75 | { 76 | data_type => "integer", 77 | is_auto_increment => 1, 78 | is_nullable => 0, 79 | sequence => "pending_blog_settings_id_seq", 80 | }, 81 | "pending_blog_id", 82 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 83 | "name", 84 | { data_type => "text", is_nullable => 0 }, 85 | "value", 86 | { 87 | data_type => "json", 88 | default_value => "{}", 89 | is_nullable => 0, 90 | serializer_class => "JSON", 91 | }, 92 | "created_at", 93 | { 94 | data_type => "timestamp with time zone", 95 | default_value => \"current_timestamp", 96 | is_nullable => 0, 97 | }, 98 | ); 99 | 100 | =head1 PRIMARY KEY 101 | 102 | =over 4 103 | 104 | =item * L 105 | 106 | =back 107 | 108 | =cut 109 | 110 | __PACKAGE__->set_primary_key("id"); 111 | 112 | =head1 UNIQUE CONSTRAINTS 113 | 114 | =head2 C 115 | 116 | =over 4 117 | 118 | =item * L 119 | 120 | =item * L 121 | 122 | =back 123 | 124 | =cut 125 | 126 | __PACKAGE__->add_unique_constraint("unq_pending_blog_id_name", ["pending_blog_id", "name"]); 127 | 128 | =head1 RELATIONS 129 | 130 | =head2 pending_blog 131 | 132 | Type: belongs_to 133 | 134 | Related object: L 135 | 136 | =cut 137 | 138 | __PACKAGE__->belongs_to( 139 | "pending_blog", 140 | "BlogDB::DB::Result::PendingBlog", 141 | { id => "pending_blog_id" }, 142 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 143 | ); 144 | 145 | 146 | # Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-12-27 00:20:15 147 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ROU+KGD7SM9g8BiTA6lz4w 148 | 149 | 150 | # You can replace this text with custom code or comments, and it will be preserved on regeneration 151 | 1; 152 | -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB/Result/PendingBlogTagMap.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package BlogDB::DB::Result::PendingBlogTagMap; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | =head1 NAME 8 | 9 | BlogDB::DB::Result::PendingBlogTagMap 10 | 11 | =cut 12 | 13 | use strict; 14 | use warnings; 15 | 16 | use base 'DBIx::Class::Core'; 17 | 18 | =head1 COMPONENTS LOADED 19 | 20 | =over 4 21 | 22 | =item * L 23 | 24 | =item * L 25 | 26 | =back 27 | 28 | =cut 29 | 30 | __PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); 31 | 32 | =head1 TABLE: C 33 | 34 | =cut 35 | 36 | __PACKAGE__->table("pending_blog_tag_map"); 37 | 38 | =head1 ACCESSORS 39 | 40 | =head2 id 41 | 42 | data_type: 'integer' 43 | is_auto_increment: 1 44 | is_nullable: 0 45 | sequence: 'pending_blog_tag_map_id_seq' 46 | 47 | =head2 blog_id 48 | 49 | data_type: 'integer' 50 | is_foreign_key: 1 51 | is_nullable: 0 52 | 53 | =head2 tag_id 54 | 55 | data_type: 'integer' 56 | is_foreign_key: 1 57 | is_nullable: 0 58 | 59 | =head2 created_at 60 | 61 | data_type: 'timestamp with time zone' 62 | default_value: current_timestamp 63 | is_nullable: 0 64 | 65 | =cut 66 | 67 | __PACKAGE__->add_columns( 68 | "id", 69 | { 70 | data_type => "integer", 71 | is_auto_increment => 1, 72 | is_nullable => 0, 73 | sequence => "pending_blog_tag_map_id_seq", 74 | }, 75 | "blog_id", 76 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 77 | "tag_id", 78 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 79 | "created_at", 80 | { 81 | data_type => "timestamp with time zone", 82 | default_value => \"current_timestamp", 83 | is_nullable => 0, 84 | }, 85 | ); 86 | 87 | =head1 PRIMARY KEY 88 | 89 | =over 4 90 | 91 | =item * L 92 | 93 | =back 94 | 95 | =cut 96 | 97 | __PACKAGE__->set_primary_key("id"); 98 | 99 | =head1 RELATIONS 100 | 101 | =head2 blog 102 | 103 | Type: belongs_to 104 | 105 | Related object: L 106 | 107 | =cut 108 | 109 | __PACKAGE__->belongs_to( 110 | "blog", 111 | "BlogDB::DB::Result::PendingBlog", 112 | { id => "blog_id" }, 113 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 114 | ); 115 | 116 | =head2 tag 117 | 118 | Type: belongs_to 119 | 120 | Related object: L 121 | 122 | =cut 123 | 124 | __PACKAGE__->belongs_to( 125 | "tag", 126 | "BlogDB::DB::Result::Tag", 127 | { id => "tag_id" }, 128 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 129 | ); 130 | 131 | 132 | # Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-11-21 05:50:00 133 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:+UAP0nhOyxxBAhXrW6c/xg 134 | 135 | 136 | # You can replace this text with custom code or comments, and it will be preserved on regeneration 137 | 1; 138 | -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB/Result/PendingTag.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package BlogDB::DB::Result::PendingTag; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | =head1 NAME 8 | 9 | BlogDB::DB::Result::PendingTag 10 | 11 | =cut 12 | 13 | use strict; 14 | use warnings; 15 | 16 | use base 'DBIx::Class::Core'; 17 | 18 | =head1 COMPONENTS LOADED 19 | 20 | =over 4 21 | 22 | =item * L 23 | 24 | =item * L 25 | 26 | =back 27 | 28 | =cut 29 | 30 | __PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); 31 | 32 | =head1 TABLE: C 33 | 34 | =cut 35 | 36 | __PACKAGE__->table("pending_tag"); 37 | 38 | =head1 ACCESSORS 39 | 40 | =head2 id 41 | 42 | data_type: 'integer' 43 | is_auto_increment: 1 44 | is_nullable: 0 45 | sequence: 'pending_tag_id_seq' 46 | 47 | =head2 name 48 | 49 | data_type: 'text' 50 | is_nullable: 0 51 | 52 | =head2 is_adult 53 | 54 | data_type: 'boolean' 55 | default_value: false 56 | is_nullable: 0 57 | 58 | =head2 created_at 59 | 60 | data_type: 'timestamp with time zone' 61 | default_value: current_timestamp 62 | is_nullable: 0 63 | 64 | =cut 65 | 66 | __PACKAGE__->add_columns( 67 | "id", 68 | { 69 | data_type => "integer", 70 | is_auto_increment => 1, 71 | is_nullable => 0, 72 | sequence => "pending_tag_id_seq", 73 | }, 74 | "name", 75 | { data_type => "text", is_nullable => 0 }, 76 | "is_adult", 77 | { data_type => "boolean", default_value => \"false", is_nullable => 0 }, 78 | "created_at", 79 | { 80 | data_type => "timestamp with time zone", 81 | default_value => \"current_timestamp", 82 | is_nullable => 0, 83 | }, 84 | ); 85 | 86 | =head1 PRIMARY KEY 87 | 88 | =over 4 89 | 90 | =item * L 91 | 92 | =back 93 | 94 | =cut 95 | 96 | __PACKAGE__->set_primary_key("id"); 97 | 98 | =head1 UNIQUE CONSTRAINTS 99 | 100 | =head2 C 101 | 102 | =over 4 103 | 104 | =item * L 105 | 106 | =back 107 | 108 | =cut 109 | 110 | __PACKAGE__->add_unique_constraint("pending_tag_name_key", ["name"]); 111 | 112 | =head1 RELATIONS 113 | 114 | =head2 tag_votes 115 | 116 | Type: has_many 117 | 118 | Related object: L 119 | 120 | =cut 121 | 122 | __PACKAGE__->has_many( 123 | "tag_votes", 124 | "BlogDB::DB::Result::TagVote", 125 | { "foreign.tag_id" => "self.id" }, 126 | { cascade_copy => 0, cascade_delete => 0 }, 127 | ); 128 | 129 | 130 | # Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-10-25 18:09:10 131 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Lax0e42Qy8BWDsYZfKmZyw 132 | 133 | 134 | sub vote_score { 135 | my ( $self ) = @_; 136 | 137 | my $score = $self->search_related('tag_votes', { vote => 1 } )->count || 0; 138 | 139 | return $score; 140 | } 141 | 1; 142 | -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB/Result/PersonFollowBlogMap.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package BlogDB::DB::Result::PersonFollowBlogMap; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | =head1 NAME 8 | 9 | BlogDB::DB::Result::PersonFollowBlogMap 10 | 11 | =cut 12 | 13 | use strict; 14 | use warnings; 15 | 16 | use base 'DBIx::Class::Core'; 17 | 18 | =head1 COMPONENTS LOADED 19 | 20 | =over 4 21 | 22 | =item * L 23 | 24 | =item * L 25 | 26 | =back 27 | 28 | =cut 29 | 30 | __PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); 31 | 32 | =head1 TABLE: C 33 | 34 | =cut 35 | 36 | __PACKAGE__->table("person_follow_blog_map"); 37 | 38 | =head1 ACCESSORS 39 | 40 | =head2 id 41 | 42 | data_type: 'integer' 43 | is_auto_increment: 1 44 | is_nullable: 0 45 | sequence: 'person_follow_blog_map_id_seq' 46 | 47 | =head2 person_id 48 | 49 | data_type: 'integer' 50 | is_foreign_key: 1 51 | is_nullable: 0 52 | 53 | =head2 blog_id 54 | 55 | data_type: 'integer' 56 | is_foreign_key: 1 57 | is_nullable: 0 58 | 59 | =head2 is_public 60 | 61 | data_type: 'boolean' 62 | default_value: false 63 | is_nullable: 0 64 | 65 | =head2 created_at 66 | 67 | data_type: 'timestamp with time zone' 68 | default_value: current_timestamp 69 | is_nullable: 0 70 | 71 | =cut 72 | 73 | __PACKAGE__->add_columns( 74 | "id", 75 | { 76 | data_type => "integer", 77 | is_auto_increment => 1, 78 | is_nullable => 0, 79 | sequence => "person_follow_blog_map_id_seq", 80 | }, 81 | "person_id", 82 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 83 | "blog_id", 84 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 85 | "is_public", 86 | { data_type => "boolean", default_value => \"false", is_nullable => 0 }, 87 | "created_at", 88 | { 89 | data_type => "timestamp with time zone", 90 | default_value => \"current_timestamp", 91 | is_nullable => 0, 92 | }, 93 | ); 94 | 95 | =head1 PRIMARY KEY 96 | 97 | =over 4 98 | 99 | =item * L 100 | 101 | =back 102 | 103 | =cut 104 | 105 | __PACKAGE__->set_primary_key("id"); 106 | 107 | =head1 RELATIONS 108 | 109 | =head2 blog 110 | 111 | Type: belongs_to 112 | 113 | Related object: L 114 | 115 | =cut 116 | 117 | __PACKAGE__->belongs_to( 118 | "blog", 119 | "BlogDB::DB::Result::Blog", 120 | { id => "blog_id" }, 121 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 122 | ); 123 | 124 | =head2 person 125 | 126 | Type: belongs_to 127 | 128 | Related object: L 129 | 130 | =cut 131 | 132 | __PACKAGE__->belongs_to( 133 | "person", 134 | "BlogDB::DB::Result::Person", 135 | { id => "person_id" }, 136 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 137 | ); 138 | 139 | 140 | # Created by DBIx::Class::Schema::Loader v0.07049 @ 2022-01-22 15:40:42 141 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:aYj6cm6mzE4v315QZI0tlg 142 | 143 | 144 | # You can replace this text with custom code or comments, and it will be preserved on regeneration 145 | 1; 146 | -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB/Result/PersonFollowPersonMap.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package BlogDB::DB::Result::PersonFollowPersonMap; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | =head1 NAME 8 | 9 | BlogDB::DB::Result::PersonFollowPersonMap 10 | 11 | =cut 12 | 13 | use strict; 14 | use warnings; 15 | 16 | use base 'DBIx::Class::Core'; 17 | 18 | =head1 COMPONENTS LOADED 19 | 20 | =over 4 21 | 22 | =item * L 23 | 24 | =item * L 25 | 26 | =back 27 | 28 | =cut 29 | 30 | __PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); 31 | 32 | =head1 TABLE: C 33 | 34 | =cut 35 | 36 | __PACKAGE__->table("person_follow_person_map"); 37 | 38 | =head1 ACCESSORS 39 | 40 | =head2 id 41 | 42 | data_type: 'integer' 43 | is_auto_increment: 1 44 | is_nullable: 0 45 | sequence: 'person_follow_person_map_id_seq' 46 | 47 | =head2 person_id 48 | 49 | data_type: 'integer' 50 | is_foreign_key: 1 51 | is_nullable: 0 52 | 53 | =head2 follow_id 54 | 55 | data_type: 'integer' 56 | is_foreign_key: 1 57 | is_nullable: 0 58 | 59 | =head2 is_public 60 | 61 | data_type: 'boolean' 62 | default_value: false 63 | is_nullable: 0 64 | 65 | =head2 created_at 66 | 67 | data_type: 'timestamp with time zone' 68 | default_value: current_timestamp 69 | is_nullable: 0 70 | 71 | =cut 72 | 73 | __PACKAGE__->add_columns( 74 | "id", 75 | { 76 | data_type => "integer", 77 | is_auto_increment => 1, 78 | is_nullable => 0, 79 | sequence => "person_follow_person_map_id_seq", 80 | }, 81 | "person_id", 82 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 83 | "follow_id", 84 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 85 | "is_public", 86 | { data_type => "boolean", default_value => \"false", is_nullable => 0 }, 87 | "created_at", 88 | { 89 | data_type => "timestamp with time zone", 90 | default_value => \"current_timestamp", 91 | is_nullable => 0, 92 | }, 93 | ); 94 | 95 | =head1 PRIMARY KEY 96 | 97 | =over 4 98 | 99 | =item * L 100 | 101 | =back 102 | 103 | =cut 104 | 105 | __PACKAGE__->set_primary_key("id"); 106 | 107 | =head1 RELATIONS 108 | 109 | =head2 follow 110 | 111 | Type: belongs_to 112 | 113 | Related object: L 114 | 115 | =cut 116 | 117 | __PACKAGE__->belongs_to( 118 | "follow", 119 | "BlogDB::DB::Result::Person", 120 | { id => "follow_id" }, 121 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 122 | ); 123 | 124 | =head2 person 125 | 126 | Type: belongs_to 127 | 128 | Related object: L 129 | 130 | =cut 131 | 132 | __PACKAGE__->belongs_to( 133 | "person", 134 | "BlogDB::DB::Result::Person", 135 | { id => "person_id" }, 136 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 137 | ); 138 | 139 | 140 | # Created by DBIx::Class::Schema::Loader v0.07049 @ 2022-01-22 15:40:42 141 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:x55oD3pQoMNTr2bnKEkzHg 142 | 143 | 144 | # You can replace this text with custom code or comments, and it will be preserved on regeneration 145 | 1; 146 | -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB/Result/PersonSetting.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package BlogDB::DB::Result::PersonSetting; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | =head1 NAME 8 | 9 | BlogDB::DB::Result::PersonSetting 10 | 11 | =cut 12 | 13 | use strict; 14 | use warnings; 15 | 16 | use base 'DBIx::Class::Core'; 17 | 18 | =head1 COMPONENTS LOADED 19 | 20 | =over 4 21 | 22 | =item * L 23 | 24 | =item * L 25 | 26 | =back 27 | 28 | =cut 29 | 30 | __PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); 31 | 32 | =head1 TABLE: C 33 | 34 | =cut 35 | 36 | __PACKAGE__->table("person_settings"); 37 | 38 | =head1 ACCESSORS 39 | 40 | =head2 id 41 | 42 | data_type: 'integer' 43 | is_auto_increment: 1 44 | is_nullable: 0 45 | sequence: 'person_settings_id_seq' 46 | 47 | =head2 person_id 48 | 49 | data_type: 'integer' 50 | is_foreign_key: 1 51 | is_nullable: 0 52 | 53 | =head2 name 54 | 55 | data_type: 'text' 56 | is_nullable: 0 57 | 58 | =head2 value 59 | 60 | data_type: 'json' 61 | default_value: '{}' 62 | is_nullable: 0 63 | serializer_class: 'JSON' 64 | 65 | =head2 created_at 66 | 67 | data_type: 'timestamp with time zone' 68 | default_value: current_timestamp 69 | is_nullable: 0 70 | 71 | =cut 72 | 73 | __PACKAGE__->add_columns( 74 | "id", 75 | { 76 | data_type => "integer", 77 | is_auto_increment => 1, 78 | is_nullable => 0, 79 | sequence => "person_settings_id_seq", 80 | }, 81 | "person_id", 82 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 83 | "name", 84 | { data_type => "text", is_nullable => 0 }, 85 | "value", 86 | { 87 | data_type => "json", 88 | default_value => "{}", 89 | is_nullable => 0, 90 | serializer_class => "JSON", 91 | }, 92 | "created_at", 93 | { 94 | data_type => "timestamp with time zone", 95 | default_value => \"current_timestamp", 96 | is_nullable => 0, 97 | }, 98 | ); 99 | 100 | =head1 PRIMARY KEY 101 | 102 | =over 4 103 | 104 | =item * L 105 | 106 | =back 107 | 108 | =cut 109 | 110 | __PACKAGE__->set_primary_key("id"); 111 | 112 | =head1 UNIQUE CONSTRAINTS 113 | 114 | =head2 C 115 | 116 | =over 4 117 | 118 | =item * L 119 | 120 | =item * L 121 | 122 | =back 123 | 124 | =cut 125 | 126 | __PACKAGE__->add_unique_constraint("unq_person_id_name", ["person_id", "name"]); 127 | 128 | =head1 RELATIONS 129 | 130 | =head2 person 131 | 132 | Type: belongs_to 133 | 134 | Related object: L 135 | 136 | =cut 137 | 138 | __PACKAGE__->belongs_to( 139 | "person", 140 | "BlogDB::DB::Result::Person", 141 | { id => "person_id" }, 142 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 143 | ); 144 | 145 | 146 | # Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-10-06 18:19:48 147 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:e4BZR6F44HnUgkm/fLp/Fw 148 | 149 | 150 | # You can replace this text with custom code or comments, and it will be preserved on regeneration 151 | 1; 152 | -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB/Result/Tag.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package BlogDB::DB::Result::Tag; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | =head1 NAME 8 | 9 | BlogDB::DB::Result::Tag 10 | 11 | =cut 12 | 13 | use strict; 14 | use warnings; 15 | 16 | use base 'DBIx::Class::Core'; 17 | 18 | =head1 COMPONENTS LOADED 19 | 20 | =over 4 21 | 22 | =item * L 23 | 24 | =item * L 25 | 26 | =back 27 | 28 | =cut 29 | 30 | __PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); 31 | 32 | =head1 TABLE: C 33 | 34 | =cut 35 | 36 | __PACKAGE__->table("tag"); 37 | 38 | =head1 ACCESSORS 39 | 40 | =head2 id 41 | 42 | data_type: 'integer' 43 | is_auto_increment: 1 44 | is_nullable: 0 45 | sequence: 'tag_id_seq' 46 | 47 | =head2 name 48 | 49 | data_type: 'text' 50 | is_nullable: 0 51 | 52 | =head2 is_adult 53 | 54 | data_type: 'boolean' 55 | default_value: false 56 | is_nullable: 0 57 | 58 | =head2 created_at 59 | 60 | data_type: 'timestamp with time zone' 61 | default_value: current_timestamp 62 | is_nullable: 0 63 | 64 | =cut 65 | 66 | __PACKAGE__->add_columns( 67 | "id", 68 | { 69 | data_type => "integer", 70 | is_auto_increment => 1, 71 | is_nullable => 0, 72 | sequence => "tag_id_seq", 73 | }, 74 | "name", 75 | { data_type => "text", is_nullable => 0 }, 76 | "is_adult", 77 | { data_type => "boolean", default_value => \"false", is_nullable => 0 }, 78 | "created_at", 79 | { 80 | data_type => "timestamp with time zone", 81 | default_value => \"current_timestamp", 82 | is_nullable => 0, 83 | }, 84 | ); 85 | 86 | =head1 PRIMARY KEY 87 | 88 | =over 4 89 | 90 | =item * L 91 | 92 | =back 93 | 94 | =cut 95 | 96 | __PACKAGE__->set_primary_key("id"); 97 | 98 | =head1 UNIQUE CONSTRAINTS 99 | 100 | =head2 C 101 | 102 | =over 4 103 | 104 | =item * L 105 | 106 | =back 107 | 108 | =cut 109 | 110 | __PACKAGE__->add_unique_constraint("tag_name_key", ["name"]); 111 | 112 | =head1 RELATIONS 113 | 114 | =head2 blog_tag_maps 115 | 116 | Type: has_many 117 | 118 | Related object: L 119 | 120 | =cut 121 | 122 | __PACKAGE__->has_many( 123 | "blog_tag_maps", 124 | "BlogDB::DB::Result::BlogTagMap", 125 | { "foreign.tag_id" => "self.id" }, 126 | { cascade_copy => 0, cascade_delete => 0 }, 127 | ); 128 | 129 | =head2 pending_blog_tag_maps 130 | 131 | Type: has_many 132 | 133 | Related object: L 134 | 135 | =cut 136 | 137 | __PACKAGE__->has_many( 138 | "pending_blog_tag_maps", 139 | "BlogDB::DB::Result::PendingBlogTagMap", 140 | { "foreign.tag_id" => "self.id" }, 141 | { cascade_copy => 0, cascade_delete => 0 }, 142 | ); 143 | 144 | 145 | # Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-11-21 05:50:00 146 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:3Y3M0cdE/p2E54iYWV0nJQ 147 | 148 | 149 | # You can replace this text with custom code or comments, and it will be preserved on regeneration 150 | 1; 151 | -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB/Result/TagVote.pm: -------------------------------------------------------------------------------- 1 | use utf8; 2 | package BlogDB::DB::Result::TagVote; 3 | 4 | # Created by DBIx::Class::Schema::Loader 5 | # DO NOT MODIFY THE FIRST PART OF THIS FILE 6 | 7 | =head1 NAME 8 | 9 | BlogDB::DB::Result::TagVote 10 | 11 | =cut 12 | 13 | use strict; 14 | use warnings; 15 | 16 | use base 'DBIx::Class::Core'; 17 | 18 | =head1 COMPONENTS LOADED 19 | 20 | =over 4 21 | 22 | =item * L 23 | 24 | =item * L 25 | 26 | =back 27 | 28 | =cut 29 | 30 | __PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); 31 | 32 | =head1 TABLE: C 33 | 34 | =cut 35 | 36 | __PACKAGE__->table("tag_vote"); 37 | 38 | =head1 ACCESSORS 39 | 40 | =head2 id 41 | 42 | data_type: 'integer' 43 | is_auto_increment: 1 44 | is_nullable: 0 45 | sequence: 'tag_vote_id_seq' 46 | 47 | =head2 tag_id 48 | 49 | data_type: 'integer' 50 | is_foreign_key: 1 51 | is_nullable: 0 52 | 53 | =head2 person_id 54 | 55 | data_type: 'integer' 56 | is_foreign_key: 1 57 | is_nullable: 0 58 | 59 | =head2 vote 60 | 61 | data_type: 'integer' 62 | default_value: 1 63 | is_nullable: 0 64 | 65 | =head2 created_at 66 | 67 | data_type: 'timestamp with time zone' 68 | default_value: current_timestamp 69 | is_nullable: 0 70 | 71 | =cut 72 | 73 | __PACKAGE__->add_columns( 74 | "id", 75 | { 76 | data_type => "integer", 77 | is_auto_increment => 1, 78 | is_nullable => 0, 79 | sequence => "tag_vote_id_seq", 80 | }, 81 | "tag_id", 82 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 83 | "person_id", 84 | { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, 85 | "vote", 86 | { data_type => "integer", default_value => 1, is_nullable => 0 }, 87 | "created_at", 88 | { 89 | data_type => "timestamp with time zone", 90 | default_value => \"current_timestamp", 91 | is_nullable => 0, 92 | }, 93 | ); 94 | 95 | =head1 PRIMARY KEY 96 | 97 | =over 4 98 | 99 | =item * L 100 | 101 | =back 102 | 103 | =cut 104 | 105 | __PACKAGE__->set_primary_key("id"); 106 | 107 | =head1 RELATIONS 108 | 109 | =head2 person 110 | 111 | Type: belongs_to 112 | 113 | Related object: L 114 | 115 | =cut 116 | 117 | __PACKAGE__->belongs_to( 118 | "person", 119 | "BlogDB::DB::Result::Person", 120 | { id => "person_id" }, 121 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 122 | ); 123 | 124 | =head2 tag 125 | 126 | Type: belongs_to 127 | 128 | Related object: L 129 | 130 | =cut 131 | 132 | __PACKAGE__->belongs_to( 133 | "tag", 134 | "BlogDB::DB::Result::PendingTag", 135 | { id => "tag_id" }, 136 | { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, 137 | ); 138 | 139 | 140 | # Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-10-06 18:19:48 141 | # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:rVX6M1gghVThPpDehmoXRg 142 | 143 | 144 | # You can replace this text with custom code or comments, and it will be preserved on regeneration 145 | 1; 146 | -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB/ResultSet/Blog.pm: -------------------------------------------------------------------------------- 1 | package BlogDB::DB::ResultSet::Blog; 2 | use warnings; 3 | use strict; 4 | use base 'DBIx::Class::ResultSet'; 5 | 6 | sub recent_entries { 7 | my ( $self, $opt ) = @_; 8 | 9 | $opt->{rows_per_page} //= 10; 10 | $opt->{page_number} //= 1; 11 | $opt->{filter_adult} //= 1; 12 | 13 | my @results; 14 | 15 | if ( $opt->{has_tag} ) { 16 | my $tag = $self->result_source->schema->resultset('Tag')->search({ name => $opt->{has_tag} })->first; 17 | 18 | if ( $tag ) { 19 | push @results, map { $_->blog } $tag->search_related('blog_tag_maps', { 20 | ( $opt->{filter_adult} ? ( 'blog.is_adult' => 0 ) : () ), 21 | }, { 22 | order_by => { -desc => 'blog.last_updated'}, 23 | rows => $opt->{rows_per_page}, 24 | offset => ( $opt->{page_number} - 1 ) * $opt->{rows_per_page}, 25 | prefetch => 'blog', 26 | })->all; 27 | 28 | my $has_next_page = $tag->search_related('blog_tag_maps', { 29 | ( $opt->{filter_adult} ? ( 'is_adult' => 0 ) : () ), 30 | }, { 31 | order_by => { -desc => 'blog.last_updated'}, 32 | rows => $opt->{rows_per_page}, 33 | offset => $opt->{page_number} * $opt->{rows_per_page}, 34 | prefetch => 'blog', 35 | })->count; 36 | 37 | return { 38 | results => \@results, 39 | has_next_page => $has_next_page >= 1 ? 1 : 0, 40 | }; 41 | } 42 | } else { 43 | push @results, $self->search({ 44 | ( $opt->{filter_adult} ? ( 'is_adult' => 0 ) : () ), 45 | }, { 46 | order_by => { -desc => 'last_updated'}, 47 | rows => $opt->{rows_per_page}, 48 | offset => ( $opt->{page_number} - 1 ) * $opt->{rows_per_page}, 49 | })->all; 50 | 51 | my $has_next_page = $self->search({ 52 | ( $opt->{filter_adult} ? ( 'is_adult' => 0 ) : () ), 53 | }, { 54 | order_by => { -desc => 'last_updated'}, 55 | rows => $opt->{rows_per_page}, 56 | offset => $opt->{page_number} * $opt->{rows_per_page}, 57 | })->count; 58 | 59 | return { 60 | results => \@results, 61 | has_next_page => $has_next_page >= 1 ? 1 : 0, 62 | }; 63 | } 64 | 65 | 66 | } 67 | 68 | 69 | 1; -------------------------------------------------------------------------------- /DB/lib/BlogDB/DB/ResultSet/BlogEntry.pm: -------------------------------------------------------------------------------- 1 | package BlogDB::DB::ResultSet::BlogEntry; 2 | use warnings; 3 | use strict; 4 | use base 'DBIx::Class::ResultSet'; 5 | 6 | sub recent_entries { 7 | my ( $self, $opt ) = @_; 8 | 9 | $opt->{rows_per_page} //= 10; 10 | $opt->{page_number} //= 1; 11 | $opt->{filter_adult} //= 1; 12 | 13 | # If we are limited to tags, we're going to turn that 14 | # into a list a blog ids, and then limit our entries search 15 | # to those blogs. If you have a better way, open a pull request. 16 | my @limit_ids; 17 | if ( $opt->{has_tag} ) { 18 | push @limit_ids, map { 19 | $_->blog_id 20 | } $self->result_source->schema->resultset('BlogTagMap')->search( 21 | { 'tag.name' => $opt->{has_tag} }, 22 | { prefetch => 'tag' }, 23 | )->all; 24 | } 25 | 26 | # If we are limited to a user's followed blogs, then we're going 27 | # to turn that into a query as well. 28 | if ( $opt->{limit_to_person_id} ) { 29 | push @limit_ids, map { 30 | $_->blog_id 31 | } $self->result_source->schema->resultset('PersonFollowBlogMap')->search( 32 | { person_id => $opt->{limit_to_person_id} }, 33 | )->all; 34 | } 35 | 36 | my @results; 37 | push @results, $self->search({ 38 | ( $opt->{filter_adult} ? ( 'blog.is_adult' => 0 ) : () ), 39 | ( @limit_ids ? ( blog_id => { -in => [ @limit_ids ] }) : () ), 40 | }, { 41 | order_by => { -desc => 'publish_date'}, 42 | rows => $opt->{rows_per_page}, 43 | offset => ( $opt->{page_number} - 1 ) * $opt->{rows_per_page}, 44 | prefetch => 'blog', 45 | })->all; 46 | 47 | # We we have a next page? 48 | 49 | my $has_next_page = $self->search({ 50 | ( $opt->{filter_adult} ? ( 'blog.is_adult' => 0 ) : () ), 51 | ( @limit_ids ? ( blog_id => { -in => [ @limit_ids ] }) : () ), 52 | }, { 53 | order_by => { -desc => 'publish_date'}, 54 | rows => $opt->{rows_per_page}, 55 | offset => ( $opt->{page_number} ) * $opt->{rows_per_page}, 56 | prefetch => 'blog', 57 | })->count; 58 | 59 | return { 60 | results => \@results, 61 | has_next_page => $has_next_page >= 1 ? 1 : 0, 62 | }; 63 | 64 | } 65 | 66 | 67 | 1; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlogDB 2 | 3 | 4 | ## Getting started 5 | 6 | BlogDB uses [Vagrant](https://www.vagrantup.com/) to create development machines. 7 | 8 | Clone this repo and run `vagrant up`. BlogDB will then be accessable at [http://localhost:8000](http://localhost:8000). 9 | 10 | 11 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # https://docs.vagrantup.com. 5 | Vagrant.configure("2") do |config| 6 | 7 | # Debian 11 box. 8 | config.vm.box = "debian/bullseye64" 9 | 10 | # Web app on port 8000 11 | config.vm.network "forwarded_port", guest: 3000, host: 8000 12 | 13 | # Include our git repo as /home/vagrant/BlogDB 14 | config.vm.synced_folder "./", "/home/vagrant/BlogDB" 15 | 16 | config.vm.provider "virtualbox" do |vb| 17 | vb.gui = false 18 | vb.name = "blogdb" 19 | vb.memory = "4096" 20 | end 21 | 22 | # Run the debian setup script, then the vagrant post install script. 23 | config.vm.provision "shell", after: "file", inline: <<-SHELL 24 | chmod 0755 /home/vagrant/BlogDB/system/setup-debian.sh 25 | chmod 0755 /home/vagrant/BlogDB/system/vagrant-post-install.sh 26 | /home/vagrant/BlogDB/system/setup-debian.sh 27 | su --login --shell /bin/bash -c '/home/vagrant/BlogDB/system/vagrant-post-install.sh' vagrant 28 | SHELL 29 | end 30 | -------------------------------------------------------------------------------- /Web/.dex.yaml: -------------------------------------------------------------------------------- 1 | - name: web 2 | desc: "Start the devel web server with plx" 3 | shell: 4 | - plx morbo ./script/blogdb_web 5 | - name: minion 6 | desc: "Start the devel minion worker with plx" 7 | shell: 8 | - plx ./script/blogdb_web minion worker 9 | -------------------------------------------------------------------------------- /Web/blogdb.docker.yml: -------------------------------------------------------------------------------- 1 | --- 2 | secrets: 3 | - oaKnK4gde163sNcpi6Kl1kZvTBx4aKHOLUOdZkSDzVl7cvgLipUyYqjvA0QGgN87 4 | 5 | database: 6 | blogdb: <%= $ENV{BDB_DEFAULT_DB} %> 7 | minion: <%= $ENV{BDB_MINION_DB} %> 8 | 9 | template_dir: simple 10 | 11 | ws_screenshot: 12 | base_url: <%= $ENV{WC_SCREENSHOT_URL} %> 13 | 14 | -------------------------------------------------------------------------------- /Web/cpanfile: -------------------------------------------------------------------------------- 1 | requires 'Minion'; 2 | requires 'Mojolicious::Plugin::XslateRenderer'; 3 | requires 'Mojolicious::Plugin::RenderFile'; 4 | requires 'Mojo::Feed'; 5 | requires 'Mojo::Pg'; 6 | requires 'DateTime::Format::Pg'; 7 | requires 'BlogDB::DB'; 8 | requires 'WebService::WsScreenshot'; 9 | requires 'LWP::UserAgent'; 10 | requires 'XML::RSS'; # This requires libexpat1-dev 11 | requires 'HTML::TreeBuilder'; 12 | 13 | test_requires "Test::Postgresql58"; 14 | test_requires "Test::More"; 15 | test_requires "Test::Deep"; 16 | -------------------------------------------------------------------------------- /Web/lib/BlogDB/Scanner.pm: -------------------------------------------------------------------------------- 1 | package BlogDB::Scanner; 2 | use Moo; 3 | use Mojo::Feed; 4 | use LWP::UserAgent; 5 | use HTML::TreeBuilder; 6 | use Try::Tiny; 7 | use URI; 8 | 9 | has ua => ( 10 | is => 'ro', 11 | init_arg => undef, 12 | default => sub { 13 | return LWP::UserAgent->new( 14 | agent => 'BlogDB::Scanner', 15 | ); 16 | } 17 | ); 18 | 19 | has url => ( is => 'rw' ); 20 | has raw => ( is => 'rw' ); 21 | has res => ( is => 'rw' ); 22 | has tree => ( is => 'rw' ); 23 | 24 | has uri => ( 25 | is => 'ro', 26 | lazy => 1, 27 | init_arg => undef, 28 | builder => sub { 29 | my ( $self ) = @_; 30 | 31 | my $uri = URI->new( $self->url ); 32 | return $uri->canonical->as_string; 33 | }, 34 | ); 35 | 36 | sub scan { 37 | my ( $self, $url ) = @_; 38 | 39 | # Promote self into an object if we were called as a class method. 40 | $self = __PACKAGE__->new 41 | unless ref($self) eq __PACKAGE__; 42 | 43 | $self->url( $url ); 44 | $self->res( $self->ua->get($url)); 45 | $self->raw( $self->res->decoded_content); 46 | $self->tree( HTML::TreeBuilder->new_from_content($self->raw)); 47 | 48 | return $self; 49 | } 50 | 51 | sub _find_meta_property { 52 | my ( $self, $property ) = @_; 53 | 54 | my ( $elem ) = $self->tree->look_down( _tag => 'meta', sub { 55 | $_[0] and 56 | $_[0]->can('attr') and 57 | $_[0]->attr('property') and 58 | $_[0]->attr('content') and 59 | $_[0]->attr('property') eq $property 60 | }); 61 | 62 | return undef unless $elem; 63 | 64 | return $elem->attr('content'); 65 | } 66 | 67 | sub title { 68 | my ( $self ) = @_; 69 | 70 | my ( $first ) = grep { defined } map { 71 | $self->_find_meta_property( $_ ) 72 | } ( qw( og:title title ) ); 73 | 74 | return $first; 75 | } 76 | 77 | # Find description -- fall back to title 78 | sub description { 79 | my ( $self ) = @_; 80 | 81 | my ( $first ) = grep { defined } map { 82 | $self->_find_meta_property( $_ ) 83 | } ( qw( og:description description og:title title ) ); 84 | 85 | return $first; 86 | } 87 | 88 | # Find the RSS URL For This Website 89 | sub rss_url { 90 | my ( $self ) = @_; 91 | 92 | my @paths = (qw( 93 | /feed 94 | /feed.rss 95 | /feed.atom 96 | /feeds/posts/default 97 | /rss.xml 98 | /atom.xml 99 | )); 100 | 101 | foreach my $path ( @paths ) { 102 | my $rss_url = $self->is_valid_rss($self->uri . $path); 103 | 104 | # We found a valid and working RSS stream. 105 | if ( $rss_url ) { 106 | return $rss_url; 107 | } 108 | } 109 | 110 | return undef; 111 | } 112 | 113 | 114 | sub is_valid_rss { 115 | my ( $self, $rss_url ) = @_; 116 | 117 | my $feed = try { 118 | Mojo::Feed->new( url => $rss_url )->is_valid; 119 | Mojo::Feed->new( url => $rss_url ); 120 | }; 121 | 122 | return unless $feed; 123 | 124 | return $feed->url if $feed->is_valid; 125 | } 126 | 127 | 1; 128 | -------------------------------------------------------------------------------- /Web/lib/BlogDB/Web/Command/scan_blogs.pm: -------------------------------------------------------------------------------- 1 | package BlogDB::Web::Command::scan_blogs; 2 | use Mojo::Base 'Mojolicious::Command'; 3 | 4 | use Mojo::Promise; 5 | use Mojo::Util qw( getopt ); 6 | 7 | has description => 'Schedule scans for new blog content.'; 8 | has usage => <<"USAGE"; 9 | $0 scan_blogs [OPTIONS] 10 | OPTIONS: 11 | -n --noop Do not schedule jobs, just show what would be done 12 | -q --quet Supress output 13 | 14 | USAGE 15 | 16 | 17 | sub run { 18 | my ( $self, @args ) = @_; 19 | 20 | getopt( \@args, 21 | 'n|noop!' => \my $noop, 22 | 'q|quiet!' => \my $quiet, 23 | ); 24 | 25 | my $app = $self->app; 26 | 27 | my $rs = $app->db->blogs->search({}); 28 | 29 | while ( my $blog = $rs->next ) { 30 | print "Checking " . $blog->title . "\n" 31 | unless $quiet; 32 | 33 | if ( ! $blog->last_updated ) { 34 | print "No last_updated date for this entry, setting to yesterday\n" 35 | unless $quiet; 36 | $blog->last_updated( DateTime->now->subtract( days => 1 )); 37 | if ( $noop ) { 38 | print "[noop] Would set last_update to yesterday.\n" 39 | unless $quiet; 40 | } else { 41 | $blog->update; 42 | } 43 | } 44 | 45 | my $delta = DateTime->now - $blog->last_updated; 46 | 47 | # How long ago was this last scanned? 48 | my ( $days, $hours )= $delta->in_units(qw( days hours )); 49 | $hours += $days * 24; 50 | 51 | # Do not update if it has been less than 12 hours since the last update. 52 | next unless $hours >= 12; 53 | 54 | # Okay, do the update. 55 | print "Creating job to update this blog.\n" 56 | unless $quiet; 57 | if ( $noop ) { 58 | print "[noop] Would enqueue this blog for update.\n" 59 | unless $quiet; 60 | } else { 61 | # TODO: The screengrab stuff should run if there was updates to the blog 62 | # entries. 63 | $app->minion->enqueue( refresh_blog_data => [ $blog->id ]); 64 | $blog->last_updated( DateTime->now ); 65 | $blog->update; 66 | } 67 | } 68 | } 69 | 70 | 1; -------------------------------------------------------------------------------- /Web/lib/BlogDB/Web/Controller/Feed.pm: -------------------------------------------------------------------------------- 1 | package BlogDB::Web::Controller::Feed; 2 | use Mojo::Base 'Mojolicious::Controller', -signatures; 3 | use Data::UUID; 4 | use URI; 5 | 6 | sub get_feed ( $c ) { 7 | $c->set_template( 'feed/index' ); 8 | 9 | my $page_number = $c->stash->{page}{number} = $c->param('page') || 1; 10 | $c->stash->{page}{has_prev} = 1 if $page_number >= 2; 11 | $c->stash->{page}{prev} = $page_number - 1; 12 | $c->stash->{page}{next} = $page_number + 1; 13 | 14 | my $recent_entries = $c->db->blog_entries->recent_entries({ 15 | filter_adult => ! $c->stash->{can_view_adult}, 16 | rows_per_page => 25, 17 | 18 | ( $page_number 19 | ? ( page_number => $page_number ) 20 | : () 21 | ), 22 | 23 | # Feed for this user. 24 | limit_to_person_id => $c->stash->{person}->id, 25 | 26 | # If we have a tag, only show entries that match it. 27 | ( $c->param('tag') 28 | ? ( has_tag => $c->param('tag') ) 29 | : ( ) 30 | ), 31 | 32 | }); 33 | 34 | push @{$c->stash->{entries}}, @{$recent_entries->{results}}; 35 | $c->stash->{page}{has_next} = $recent_entries->{has_next_page}; 36 | 37 | push @{$c->stash->{tags_a}}, grep { $_->id % 2 == 1 } $c->db->tags->search({ 38 | ( ! $c->stash->{can_view_adult} ? ( is_adult => 0 ) : () ), 39 | })->all; 40 | 41 | push @{$c->stash->{tags_b}}, grep { $_->id % 2 == 0 } $c->db->tags->search({ 42 | ( ! $c->stash->{can_view_adult} ? ( is_adult => 0 ) : () ), 43 | })->all; 44 | } 45 | 46 | 47 | 1; -------------------------------------------------------------------------------- /Web/lib/BlogDB/Web/Test.pm: -------------------------------------------------------------------------------- 1 | package BlogDB::Web::Test; 2 | use Import::Into; 3 | use Test::More; 4 | use Test::Deep; 5 | use Test::Mojo::BlogDB; 6 | use Test::Postgresql58; 7 | 8 | push our @ISA, qw( Exporter ); 9 | push our @EXPORT, qw( $run_code ); 10 | 11 | sub import { 12 | shift->export_to_level(1); 13 | my $target = caller; 14 | 15 | Mojo::Base ->import::into($target, '-strict', '-signatures' ); 16 | warnings ->import::into($target); 17 | strict ->import::into($target); 18 | Test::More ->import::into($target); 19 | Test::Deep ->import::into($target); 20 | Test::Mojo::BlogDB->import::into($target); 21 | } 22 | 23 | our $pgsql = Test::Postgresql58->new() 24 | or BAILOUT( "PSQL Error: " . $Test::Postgresql58::errstr ); 25 | 26 | load_psql_file("../DB/etc/schema.sql"); 27 | 28 | $ENV{BLOGDB_TESTMODE} = 1; 29 | $ENV{BLOGDB_DSN} = $pgsql->dsn; 30 | 31 | sub load_psql_file { 32 | my ( $file ) = @_; 33 | 34 | open my $lf, "<", $file 35 | or die "Failed to open $file for reading: $!"; 36 | my $content; 37 | while ( defined( my $line = <$lf> ) ) { 38 | next unless $line !~ /^\s*--/; 39 | $content .= $line; 40 | } 41 | close $lf; 42 | 43 | my $dbh = DBI->connect( $pgsql->dsn ); 44 | for my $command ( split( /;/, $content ) ) { 45 | next if $command =~ /^\s*$/; 46 | $dbh->do( $command ) 47 | or BAIL_OUT( "PSQL Error($file): $command: " . $dbh->errstr); 48 | } 49 | undef $dbh; 50 | } 51 | 52 | 1; -------------------------------------------------------------------------------- /Web/lib/Test/Mojo/BlogDB.pm: -------------------------------------------------------------------------------- 1 | package Test::Mojo::BlogDB; 2 | # This package subclasses Test::Mojo and sets up some 3 | # functionality. 4 | # 5 | # _stash_ 6 | # The $t object will now have a stash method that returns 7 | # the stash. 8 | # 9 | # _code_block_ 10 | # The $t object will now have a code_block method that 11 | # accepts a code block, runs it, and returns $t. 12 | # 13 | # This combination of stash and code_block enable a 14 | # pattern like the following: 15 | # 16 | # $t->post_ok( '/login', { user => $user, pass => $pass}) 17 | # ->code_block( sub { 18 | # my $t = shift; 19 | # is($t->stash->{person}->user, $user, "User saved in stash."); 20 | # })->status_is( 200 ); 21 | # 22 | # _dump_stash_ 23 | # The $t object now has a dump_stash method that prints the 24 | # stash to STDERR. By default this will supress mojo-specific 25 | # stash elements, pass a true value to dump the full stash. 26 | # 27 | # $t->get_ok('/') 28 | # ->dump_stash(1) 29 | # ->status_is(200); 30 | # 31 | use warnings; 32 | use strict; 33 | use parent 'Test::Mojo'; 34 | use Data::Dumper; 35 | use Test::Deep; 36 | use Test::More; 37 | 38 | sub new { 39 | my $class = shift; 40 | my $self = $class->SUPER::new(@_); 41 | 42 | $self->app->hook( after_dispatch => sub { 43 | my ( $c ) = @_; 44 | $self->stash( $c->stash ); 45 | }); 46 | 47 | return $self; 48 | } 49 | 50 | sub stash { 51 | my $self = shift; 52 | $self->{stash} = shift if @_; 53 | return $self->{stash}; 54 | } 55 | 56 | sub code_block { 57 | my ( $t, $code ) = @_; 58 | 59 | $code->($t); 60 | 61 | return $t; 62 | } 63 | 64 | sub dump_stash { 65 | my ( $t, $show_all ) = @_; 66 | 67 | if ( $show_all ) { 68 | warn Dumper $t->stash; 69 | return $t; 70 | } 71 | 72 | my $ds; 73 | 74 | foreach my $key ( keys %{$t->stash}) { 75 | next if $key eq 'controller'; 76 | next if $key eq 'action'; 77 | next if $key eq 'cb'; 78 | next if $key eq 'template'; 79 | next if $key eq 'person'; 80 | next if $key =~ m|^mojo\.|; 81 | 82 | $ds->{$key} = $t->stash->{$key}; 83 | } 84 | 85 | warn Dumper $ds; 86 | 87 | return $t; 88 | } 89 | 90 | sub stash_has { 91 | my ( $t, $expect, $desc ) = @_; 92 | 93 | cmp_deeply( $t->stash, superhashof($expect), $desc); 94 | 95 | return $t; 96 | } 97 | 98 | sub create_user { 99 | my ( $t, $settings ) = @_; 100 | 101 | my $user = join( '', map({ ('a'..'z','A'..'Z')[int rand 52] } ( 0 .. 8)) ); 102 | $t->post_ok( '/register', form => { 103 | username => $user, 104 | email => "$user\@blogdb.com", 105 | password => $user, 106 | confirm => $user, 107 | }) 108 | ->get_ok( '/') 109 | ->code_block( sub { 110 | is(shift->stash->{person}->username, $user, "Created test user $user"); 111 | }) 112 | ->code_block( sub { 113 | # Now we're just gonna add whatever settings from $settings. 114 | my $t = shift; 115 | foreach my $key ( keys %{$settings || {}}) { 116 | $t->stash->{person}->setting( $key, $settings->{$key}); 117 | } 118 | }); 119 | 120 | return $t; 121 | } 122 | 123 | sub create_tag { 124 | my ( $t, $tag, $is_adult ) = @_; 125 | 126 | $t->post_ok( '/tags/suggest', form => { 127 | tag => $tag, 128 | ($is_adult ? ( is_adult => 1 ) : ( ) ), 129 | }); 130 | 131 | return $t; 132 | } 133 | 134 | # Storage stack for a run, convenience for stashing stuff. 135 | sub _ss { 136 | my ( $t, $data_stack ) = @_; 137 | $t->{data_stack} = $data_stack; 138 | return $t; 139 | } 140 | 141 | sub _sg { 142 | return shift->{data_stack}; 143 | } 144 | 145 | 1; -------------------------------------------------------------------------------- /Web/script/blogdb_web: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | 5 | use Mojo::File qw(curfile); 6 | use lib curfile->dirname->sibling('lib')->to_string; 7 | use Mojolicious::Commands; 8 | 9 | # Start command line interface for application 10 | Mojolicious::Commands->start_app('BlogDB::Web'); 11 | -------------------------------------------------------------------------------- /Web/script/dbc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | #== 3 | # Database Connect - exec psql with the credentials from blogdb.yml to get a prompt. 4 | #== 5 | use warnings; 6 | use strict; 7 | use DBIx::Class::Schema::Config; 8 | use CPAN::Meta::YAML; 9 | 10 | my ( $file ) = grep { -e $_ } ( qw( 11 | /home/blogdb/BlogDB/blogdb.yml 12 | /home/vagrant/BlogDB/blogdb.yml 13 | blogdb.yml 14 | Web/blogdb.yml 15 | ../blogdb.yml 16 | )); 17 | 18 | open my $lf, '<', $file 19 | or die "Failed to open $file for read: $!"; 20 | my $content = do { local $/; <$lf> }; 21 | close $lf; 22 | 23 | my $data = CPAN::Meta::YAML->read_string( $content ); 24 | 25 | my $config = DBIx::Class::Schema::Config->coerce_credentials_from_mojolike( 26 | DBIx::Class::Schema::Config->_make_connect_attrs($data->[0]{database}{blogdb}) 27 | ); 28 | 29 | $ENV{PGPASSWORD} = $config->{password}; 30 | exec qw( psql -h localhost -U blogdb blogdb ); 31 | -------------------------------------------------------------------------------- /Web/t/01_endpoints/01_root/01_register.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base '-signatures'; 3 | use BlogDB::Web::Test; 4 | 5 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 6 | 7 | # Try creating an account with an error, ensure we get the error. 8 | $t->post_ok( '/register', form => { 9 | username => 'fred', 10 | email => 'fred@blog.com', 11 | password => 'SuperSecure', 12 | confirm => 'SuperFail', 13 | })->status_is( 200 14 | )->code_block( sub { 15 | is( shift->stash->{errors}->[0], 'Password & Confirmation must match.', 'Expected error thrown' ); 16 | })->code_block( sub { 17 | is( shift->app->db->resultset('Person')->search( { username => 'fred'})->count, 0, 'No user created.'); 18 | }); 19 | 20 | # Try creating a valid account, ensure it exists in the DB. 21 | $t->post_ok( '/register', form => { 22 | username => 'fred', 23 | email => 'fred@blog.com', 24 | password => 'SuperSecure', 25 | confirm => 'SuperSecure', 26 | })->status_is( 302 27 | )->code_block( sub { 28 | is( scalar(@{shift->stash->{errors}}), 0, 'No errors' ); 29 | })->code_block( sub { 30 | is( shift->app->db->resultset('Person')->search( { username => 'fred'})->count, 1, 'User created.'); 31 | })->get_ok( '/' 32 | )->code_block( sub { 33 | is(shift->stash->{person}->username, 'fred', 'Got the fred after login...'); 34 | })->stash_has( { blogs => [ ]}, "Blog entry array ref exists."); 35 | 36 | done_testing(); -------------------------------------------------------------------------------- /Web/t/01_endpoints/01_root/02_login.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base '-signatures'; 3 | use BlogDB::Web::Test; 4 | 5 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 6 | 7 | # Try creating a valid account, ensure it exists in the DB. 8 | $t->post_ok( '/register', form => { 9 | username => 'fred', 10 | email => 'fred@blog.com', 11 | password => 'SuperSecure', 12 | confirm => 'SuperSecure', 13 | })->status_is( 302 14 | )->code_block( sub { 15 | is( scalar(@{shift->stash->{errors}}), 0, 'No errors' ); 16 | })->code_block( sub { 17 | is( shift->app->db->resultset('Person')->search( { username => 'fred'})->count, 1, 'User created.'); 18 | })->get_ok( '/' 19 | )->code_block( sub { 20 | is(shift->stash->{person}->username, 'fred', 'Got the fred after login...'); 21 | })->stash_has( { blogs => [ ]}, "Blog entry array ref exists."); 22 | 23 | # New Session, verify it isn't logged in. 24 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 25 | 26 | $t->get_ok( '/' )->code_block( sub { is(shift->stash->{person}, undef, "Not logged in.") }); 27 | 28 | $t->post_ok( '/login', form => { username => 'fred', password => 'SuperSecure'}) 29 | ->get_ok( '/') 30 | ->code_block( sub { is(shift->stash->{person}->username, 'fred', 'Logged in')}); 31 | 32 | done_testing(); -------------------------------------------------------------------------------- /Web/t/01_endpoints/01_root/03_logout.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base '-signatures'; 3 | use BlogDB::Web::Test; 4 | 5 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 6 | 7 | # Try creating a valid account, ensure it exists in the DB. 8 | $t->post_ok( '/register', form => { 9 | username => 'fred', 10 | email => 'fred@blog.com', 11 | password => 'SuperSecure', 12 | confirm => 'SuperSecure', 13 | })->status_is( 302 14 | )->code_block( sub { 15 | is( scalar(@{shift->stash->{errors}}), 0, 'No errors' ); 16 | })->code_block( sub { 17 | is( shift->app->db->resultset('Person')->search( { username => 'fred'})->count, 1, 'User created.'); 18 | })->get_ok( '/' 19 | )->code_block( sub { 20 | is(shift->stash->{person}->username, 'fred', 'Got the fred after login...'); 21 | })->stash_has( { blogs => [ ]}, "Blog entry array ref exists."); 22 | 23 | # New Session, verify it isn't logged in. 24 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 25 | 26 | $t->get_ok( '/' )->code_block( sub { is(shift->stash->{person}, undef, "Not logged in.") }); 27 | 28 | $t->post_ok( '/login', form => { username => 'fred', password => 'SuperSecure'}) 29 | ->get_ok( '/') 30 | ->code_block( sub { is(shift->stash->{person}->username, 'fred', 'Logged in')}); 31 | 32 | $t->post_ok('/logout') 33 | ->get_ok( '/') 34 | ->code_block( sub { is(shift->stash->{person}, undef, 'User logged out.')}); 35 | 36 | done_testing(); -------------------------------------------------------------------------------- /Web/t/01_endpoints/01_root/04_forgot_password.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base '-signatures'; 3 | use BlogDB::Web::Test; 4 | 5 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 6 | 7 | # Try creating a valid account, ensure it exists in the DB. 8 | $t->post_ok( '/register', form => { 9 | username => 'fred', 10 | email => 'fred@blog.com', 11 | password => 'SuperSecure', 12 | confirm => 'SuperSecure', 13 | })->status_is( 302 14 | )->code_block( sub { 15 | is( scalar(@{shift->stash->{errors}}), 0, 'No errors' ); 16 | })->code_block( sub { 17 | is( shift->app->db->resultset('Person')->search( { username => 'fred'})->count, 1, 'User created.'); 18 | })->get_ok( '/' 19 | )->code_block( sub { 20 | is(shift->stash->{person}->username, 'fred', 'Got the fred after login...'); 21 | })->stash_has( { blogs => [ ]}, "Blog entry array ref exists."); 22 | 23 | # New Session, verify it isn't logged in. 24 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 25 | 26 | $t->get_ok( '/' )->code_block( sub { is(shift->stash->{person}, undef, "Not logged in.") }); 27 | 28 | $t->post_ok( '/forgot', form => { username => 'noone' }) 29 | ->status_is( 200 ) 30 | ->code_block( sub { 31 | is(shift->stash->{errors}->[0], 'No such username or email address.', 'Invalid addresses raise errors.'); 32 | }); 33 | 34 | $t->post_ok( '/forgot', form => { username => 'fred' }) 35 | ->status_is( 200 ) 36 | ->stash_has( { success => 1}, "Finished setting token.") 37 | ->code_block( sub { 38 | my $t = shift; 39 | ok my $fred = $t->app->db->resultset('Person')->find( { username => 'fred' }), "Found fred in DB."; 40 | ok my $reset_token = $fred->search_related('password_tokens')->first->token, "Found reset token in DB."; 41 | $t->stash( { %{$t->stash}, token => $reset_token } ) 42 | }); 43 | 44 | done_testing(); 45 | -------------------------------------------------------------------------------- /Web/t/01_endpoints/01_root/05_reset_password.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base '-signatures'; 3 | use BlogDB::Web::Test; 4 | 5 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 6 | 7 | # Try creating a valid account, ensure it exists in the DB. 8 | $t->post_ok( '/register', form => { 9 | username => 'fred', 10 | email => 'fred@blog.com', 11 | password => 'SuperSecure', 12 | confirm => 'SuperSecure', 13 | })->status_is( 302 14 | )->code_block( sub { 15 | is( scalar(@{shift->stash->{errors}}), 0, 'No errors' ); 16 | })->code_block( sub { 17 | is( shift->app->db->resultset('Person')->search( { username => 'fred'})->count, 1, 'User created.'); 18 | })->get_ok( '/' 19 | )->code_block( sub { 20 | is(shift->stash->{person}->username, 'fred', 'Got the fred after login...'); 21 | })->stash_has( { blogs => [ ]}, "Blog entry array ref exists."); 22 | 23 | # New Session, verify it isn't logged in. 24 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 25 | $t->get_ok( '/' )->code_block( sub { is(shift->stash->{person}, undef, "Not logged in.") }); 26 | 27 | # Fill out the forgot password form to get a token. 28 | $t->post_ok( '/forgot', form => { username => 'fred' }) 29 | ->status_is( 200 ) 30 | ->stash_has( { success => 1}, "Finished setting token.") 31 | ->code_block( sub { 32 | my $t = shift; 33 | ok my $fred = $t->app->db->resultset('Person')->find( { username => 'fred' }), "Found fred in DB."; 34 | ok my $reset_token = $fred->search_related('password_tokens')->first->token, "Found reset token in DB."; 35 | $t->stash( { %{$t->stash}, token => $reset_token } ) 36 | }); 37 | 38 | my $reset_token = $t->stash->{token}; 39 | 40 | # Use the reset password form to reset the password. 41 | $t->post_ok( "/forgot/$reset_token", form => { 42 | reset_token => $reset_token, 43 | password => 'NewPassword', 44 | confirm => 'NewPassword', 45 | })->stash_has( { success => 1 }); 46 | 47 | # New Session, verify it isn't logged in. 48 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 49 | $t->get_ok( '/' )->code_block( sub { is(shift->stash->{person}, undef, "Not logged in.") }); 50 | 51 | # Prove the login doesn't work with the old password. 52 | $t->post_ok( '/login', form => { username => 'fred', password => 'SuperSecure'}) 53 | ->get_ok( '/') 54 | ->code_block( sub { is( shift->stash->{person}, undef, "Old password doesn't work.")}); 55 | 56 | # Prove the login works with the new password. 57 | $t->post_ok( '/login', form => { username => 'fred', password => 'NewPassword'}) 58 | ->get_ok( '/') 59 | ->code_block( sub { is( shift->stash->{person}->username, 'fred', "New password does work.")}); 60 | 61 | done_testing(); -------------------------------------------------------------------------------- /Web/t/01_endpoints/02_tags/01_suggest.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base '-signatures'; 3 | use BlogDB::Web::Test; 4 | 5 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 6 | 7 | # Suggesting a tag without a user account will not work. 8 | $t->post_ok( '/tags/suggest', form => { 9 | tag => 'foo', 10 | })->stash_has( { errors => [ 'Login required.' ]}, 'Prompt for login.'); 11 | 12 | # Suggesting a tag with a user account will work, the tag will be in the DB in PendingTag. 13 | $t->create_user->post_ok( '/tags/suggest', form => { 14 | tag => 'foo', 15 | })->code_block( sub { 16 | my ( $t ) = @_; 17 | $t->_ss( $t->app->db->resultset('PendingTag')->search( { name => 'foo'})->first); 18 | ok $t->_sg, "The PendingTag was created."; 19 | is $t->_sg->name, 'foo', "Pending tag named correctly."; 20 | is $t->_sg->is_adult, 0, 'Tags are not adult by default.'; 21 | })->status_is( 302, "Redirect after tag add." ) 22 | ->post_ok( '/tags/suggest', form => { 23 | tag => 'foo', 24 | })->stash_has( { errors => [ 'There is already a pending tag with that name.']}, 25 | 'Duplicate tag results in error' 26 | )->status_is( 200, "Stay on page during an error." ); 27 | 28 | # Suggesting the same tag again will result in an error because the tag already exists. 29 | $t->post_ok( '/tags/suggest', form => { 30 | tag => '9foo', 31 | })->stash_has( { errors => [ 'Tag names must start with a letter, and may only contain letters and numbers.']}, 32 | 'Tags starting with numbers result in errors.' 33 | )->status_is( 200, "Stay on page during an error." ); 34 | 35 | # Suggesting a tag that's an adult tag will make one with is_adult = true 36 | $t->post_ok( '/tags/suggest', form => { 37 | tag => 'adult_tag', 38 | is_adult => 1, 39 | })->code_block( sub { 40 | my ( $t ) = @_; 41 | $t->_ss( $t->app->db->resultset('PendingTag')->search( { name => 'adult_tag'})->first); 42 | ok $t->_sg, "The PendingTag was created."; 43 | is $t->_sg->name, 'adult_tag', "Pending tag named correctly."; 44 | is $t->_sg->is_adult, 1, 'Tag is set as an adult tag.'; 45 | })->status_is( 302, "Redirect after tag add." ); 46 | 47 | done_testing(); -------------------------------------------------------------------------------- /Web/t/01_endpoints/02_tags/02_vote.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base '-signatures'; 3 | use BlogDB::Web::Test; 4 | 5 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 6 | 7 | 8 | # Create a tag, it should have no votes at first. 9 | $t->create_user->create_tag( 'first' )->code_block( sub { 10 | my $t = shift; 11 | $t->_ss( $t->app->db->resultset('PendingTag')->search({name => 'first'})->first); 12 | ok $t->_sg, "Found tag"; 13 | is $t->_sg->name, 'first', "Tag named correctly."; 14 | is $t->_sg->vote_score, 0, "Vote score = 0"; 15 | }); 16 | 17 | # Upvote them! 18 | $t->post_ok( '/tags/vote', form => { 19 | tag => 'first' 20 | })->code_block( sub { 21 | my $t = shift; 22 | $t->_ss( $t->app->db->resultset('PendingTag')->search({name => 'first'})->first); 23 | ok $t->_sg, "Found tag"; 24 | is $t->_sg->name, 'first', "Tag named correctly."; 25 | is $t->_sg->vote_score, 1, "Vote score = 1"; 26 | }); 27 | 28 | # Upvote them again to undo it! 29 | $t->post_ok( '/tags/vote', form => { 30 | tag => 'first' 31 | })->code_block( sub { 32 | my $t = shift; 33 | $t->_ss( $t->app->db->resultset('PendingTag')->search({name => 'first'})->first); 34 | ok $t->_sg, "Found tag"; 35 | is $t->_sg->name, 'first', "Tag named correctly."; 36 | is $t->_sg->vote_score, 0, "Vote score = 0"; 37 | }); 38 | 39 | # Make three new users and upvote it, then check the vote count. 40 | $t = Test::Mojo::BlogDB->new('BlogDB::Web') 41 | ->create_user 42 | ->post_ok( '/tags/vote', form => { tag => 'first'}); 43 | 44 | $t = Test::Mojo::BlogDB->new('BlogDB::Web') 45 | ->create_user 46 | ->post_ok( '/tags/vote', form => { tag => 'first'}); 47 | 48 | $t = Test::Mojo::BlogDB->new('BlogDB::Web') 49 | ->create_user 50 | ->post_ok( '/tags/vote', form => { tag => 'first'}) 51 | ->code_block( sub { 52 | my $t = shift; 53 | $t->_ss( $t->app->db->resultset('PendingTag')->search({name => 'first'})->first); 54 | ok $t->_sg, "Found tag"; 55 | is $t->_sg->name, 'first', "Tag named correctly."; 56 | is $t->_sg->vote_score, 3, "Vote score = 3"; 57 | }); 58 | 59 | # Voting on a tag without a user account will not work. 60 | $t = Test::Mojo::BlogDB->new('BlogDB::Web') 61 | ->post_ok( '/tags/vote', form => { tag => 'first'}) 62 | ->stash_has( { errors => [ 'Login required.' ] }, 'Need user account to vote.' ) 63 | ->code_block( sub { 64 | my $t = shift; 65 | $t->_ss( $t->app->db->resultset('PendingTag')->search({name => 'first'})->first); 66 | ok $t->_sg, "Found tag"; 67 | is $t->_sg->name, 'first', "Tag named correctly."; 68 | is $t->_sg->vote_score, 3, "Same vote count after unauthed vote attempt."; 69 | }); 70 | 71 | 72 | done_testing; -------------------------------------------------------------------------------- /Web/t/01_endpoints/02_tags/03_approve.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base '-signatures'; 3 | use BlogDB::Web::Test; 4 | 5 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 6 | 7 | # Create a user and a tag. 8 | $t->create_user->create_tag( 'first' )->code_block( sub { 9 | my $t = shift; 10 | $t->_ss( $t->app->db->resultset('PendingTag')->search({name => 'first'})->first); 11 | ok $t->_sg, "Found tag"; 12 | is $t->_sg->name, 'first', "Tag named correctly."; 13 | is $t->_sg->vote_score, 0, "Vote score = 0"; 14 | })->create_tag( 'second' )->create_tag( 'third'); 15 | 16 | # The user cannot approve the tag. 17 | $t->post_ok( '/tags/approve', form => { tag => 'first'}) 18 | ->code_block( sub { 19 | my $t = shift; 20 | $t->_ss( $t->app->db->resultset('Tag')->search({name => 'first'})->first); 21 | is $t->_sg, undef, "The tag has not been approved."; 22 | }) 23 | ->stash_has( { errors => [ 'Not authorized.' ] }, 'Rejected error message.'); 24 | 25 | # A user who is not logged in cannot approve the tag. 26 | $t = Test::Mojo::BlogDB->new('BlogDB::Web') 27 | ->post_ok( '/tags/approve', form => { tag => 'second'}) 28 | ->code_block( sub { 29 | my $t = shift; 30 | $t->_ss( $t->app->db->resultset('Tag')->search({name => 'second'})->first); 31 | is $t->_sg, undef, "The tag has not been approved."; 32 | }); 33 | 34 | # A user with the setting can_add_tags can approve the tag. 35 | $t->create_user( { can_manage_tags => 1 } ) 36 | ->post_ok( '/tags/approve', form => { tag => 'second'} ) 37 | ->code_block( sub { 38 | is( shift->stash->{person}->setting('can_manage_tags'), 1, "User has permission."); 39 | }) 40 | ->code_block( sub { 41 | my $t = shift; 42 | $t->_ss( $t->app->db->resultset('Tag')->search({name => 'second'})->first); 43 | ok $t->_sg, "The tag exists in the Tag list."; 44 | $t->_ss( $t->app->db->resultset('PendingTag')->search({name => 'second'})->first); 45 | is $t->_sg, undef, "The tag has been deleted from the PendingTag list."; 46 | }); 47 | 48 | done_testing; -------------------------------------------------------------------------------- /Web/t/01_endpoints/02_tags/04_delete.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base '-signatures'; 3 | use BlogDB::Web::Test; 4 | 5 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 6 | 7 | # Create a user and a tag. 8 | $t->create_user->create_tag( 'first' )->code_block( sub { 9 | my $t = shift; 10 | $t->_ss( $t->app->db->resultset('PendingTag')->search({name => 'first'})->first); 11 | ok $t->_sg, "Found tag"; 12 | is $t->_sg->name, 'first', "Tag named correctly."; 13 | is $t->_sg->vote_score, 0, "Vote score = 0"; 14 | })->create_tag( 'second' )->create_tag( 'third'); 15 | 16 | # The user cannot delete the tag. 17 | $t->post_ok( '/tags/delete', form => { tag => 'first'}) 18 | ->code_block( sub { 19 | my $t = shift; 20 | $t->_ss( $t->app->db->resultset('PendingTag')->search({name => 'first'})->first); 21 | ok $t->_sg, "The tag exists and has not been deleted."; 22 | }) 23 | ->stash_has( { errors => [ 'Not authorized.' ] }, 'Rejected error message.'); 24 | 25 | # A user who is not logged in cannot delete the tag. 26 | $t = Test::Mojo::BlogDB->new('BlogDB::Web') 27 | ->post_ok( '/tags/delete', form => { tag => 'first'}) 28 | ->code_block( sub { 29 | my $t = shift; 30 | $t->_ss( $t->app->db->resultset('PendingTag')->search({name => 'first'})->first); 31 | ok $t->_sg, "The tag exists and has not been deleted."; 32 | }); 33 | 34 | # A user with the setting can_manage_tags can delete the tag. 35 | $t->create_user( { can_manage_tags => 1 } ) 36 | ->post_ok( '/tags/delete', form => { tag => 'second'} ) 37 | ->code_block( sub { 38 | is( shift->stash->{person}->setting('can_manage_tags'), 1, "User has permission."); 39 | }) 40 | ->code_block( sub { 41 | my $t = shift; 42 | $t->_ss( $t->app->db->resultset('PendingTag')->search({name => 'second'})->first); 43 | is $t->_sg, undef, "The tag has been deleted from the PendingTag list."; 44 | }); 45 | 46 | done_testing; -------------------------------------------------------------------------------- /Web/t/01_endpoints/03_blog/01_new_blog.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base '-signatures'; 3 | use BlogDB::Web::Test; 4 | 5 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 6 | 7 | # Post a new blog as a logged in user, ensure that it exists, 8 | # make sure that the submitter id matches. 9 | $t->create_user->post_ok( '/blog/new', 10 | form => { 11 | url => 'https://modfoss.com/', 12 | })->code_block( sub { 13 | my ( $t ) = @_; 14 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 15 | ok $t->_sg, "Created blog entry."; 16 | is $t->_sg->submitter_id, $t->stash->{person}->id, 'Owned by the current user.'; 17 | is $t->_sg->edit_token, undef, 'No edit token for a user-submitted blog.'; 18 | }); 19 | 20 | # Post a new blog as an anonymous user, ensure that it exists, 21 | # and an edit token has been created for it. 22 | Test::Mojo::BlogDB->new('BlogDB::Web')->post_ok( '/blog/new', 23 | form => { 24 | url => 'https://symkat.com/', 25 | })->code_block( sub { 26 | my ( $t ) = @_; 27 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://symkat.com/'})); 28 | ok $t->_sg, "Created blog entry."; 29 | is $t->_sg->submitter_id, undef, 'No submitter id for anonymous submitted blog.'; 30 | ok $t->_sg->edit_token, 'Edit token for a anonymous-submitted blog exists.'; 31 | }); 32 | 33 | done_testing; -------------------------------------------------------------------------------- /Web/t/01_endpoints/03_blog/02_edit_new_blog/01_as_user.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # Test that a new blog submitted by a logged in user can be edited by that same user. 3 | use Mojo::Base '-signatures'; 4 | use BlogDB::Web::Test; 5 | 6 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 7 | 8 | # Post a new blog as a logged in user, ensure that it exists, make sure that the submitter id matches. 9 | $t->create_user->post_ok( '/blog/new', 10 | form => { 11 | url => 'https://modfoss.com/', 12 | })->code_block( sub { 13 | my ( $t ) = @_; 14 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 15 | ok $t->_sg, "Created blog entry."; 16 | is $t->_sg->submitter_id, $t->stash->{person}->id, 'Owned by the current user.'; 17 | is $t->_sg->edit_token, undef, 'No edit token for a user-submitted blog.'; 18 | }); 19 | 20 | my $blog_id = $t->_sg->id; 21 | 22 | $t->post_ok( "/blog/new/$blog_id", form => { 23 | title => 'modFoss', 24 | url => 'https://modfoss.com/', 25 | rss_url => 'https://modfoss.com/feed', 26 | tagline => 'Articles on technical matters.', 27 | about => 'A technical blog.' 28 | })->code_block( sub { 29 | my ( $t ) = @_; 30 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 31 | ok $t->_sg, "Found blog entry"; 32 | is $t->_sg->title, 'modFoss', 'Title updated.'; 33 | is $t->_sg->url, 'https://modfoss.com/', 'URL updated.'; 34 | is $t->_sg->rss_url , 'https://modfoss.com/feed', 'RSS URL updated.'; 35 | is $t->_sg->tagline , 'Articles on technical matters.', 'Tagline updated.'; 36 | is $t->_sg->about , 'A technical blog.', 'About updated.'; 37 | })->stash_has( { authorization => [ 'submitter' ] } ); 38 | 39 | done_testing; -------------------------------------------------------------------------------- /Web/t/01_endpoints/03_blog/02_edit_new_blog/02_with_token.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # 3 | # Post a new blog without any user account, and then edit the blog. 4 | # Make sure the edit_token functionality is being used. 5 | use Mojo::Base '-signatures'; 6 | use BlogDB::Web::Test; 7 | 8 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 9 | 10 | $t->post_ok( '/blog/new', 11 | form => { 12 | url => 'https://modfoss.com/', 13 | })->code_block( sub { 14 | my ( $t ) = @_; 15 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 16 | ok $t->_sg, "Created blog entry."; 17 | ok $t->_sg->edit_token, 'Edit token exists'; 18 | }); 19 | 20 | my $blog_id = $t->_sg->id; 21 | 22 | $t->post_ok( "/blog/new/$blog_id", form => { 23 | title => 'modFoss', 24 | url => 'https://modfoss.com/', 25 | rss_url => 'https://modfoss.com/feed', 26 | tagline => 'Articles on technical matters.', 27 | about => 'A technical blog.' 28 | })->code_block( sub { 29 | my ( $t ) = @_; 30 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 31 | ok $t->_sg, "Found blog entry"; 32 | is $t->_sg->title, 'modFoss', 'Title updated.'; 33 | is $t->_sg->url, 'https://modfoss.com/', 'URL updated.'; 34 | is $t->_sg->rss_url , 'https://modfoss.com/feed', 'RSS URL updated.'; 35 | is $t->_sg->tagline , 'Articles on technical matters.', 'Tagline updated.'; 36 | is $t->_sg->about , 'A technical blog.', 'About updated.'; 37 | })->stash_has( { authorization => [ 'token' ] } ); 38 | 39 | done_testing; -------------------------------------------------------------------------------- /Web/t/01_endpoints/03_blog/02_edit_new_blog/03_with_can_manage_blogs.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base '-signatures'; 3 | use BlogDB::Web::Test; 4 | 5 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 6 | 7 | # Post a new blog as a logged in user, ensure that it exists, make sure that the submitter id matches. 8 | $t->create_user->post_ok( '/blog/new', 9 | form => { 10 | url => 'https://modfoss.com/', 11 | })->code_block( sub { 12 | my ( $t ) = @_; 13 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 14 | ok $t->_sg, "Created blog entry."; 15 | is $t->_sg->submitter_id, $t->stash->{person}->id, 'Owned by the current user.'; 16 | is $t->_sg->edit_token, undef, 'No edit token for a user-submitted blog.'; 17 | }); 18 | 19 | my $blog_id = $t->_sg->id; 20 | 21 | # New Session. 22 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 23 | 24 | $t->create_user({ can_manage_blogs => 1 })->post_ok( "/blog/new/$blog_id", form => { 25 | title => 'modFoss', 26 | url => 'https://modfoss.com/', 27 | rss_url => 'https://modfoss.com/feed', 28 | tagline => 'Articles on technical matters.', 29 | about => 'A technical blog.' 30 | })->code_block( sub { 31 | my ( $t ) = @_; 32 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 33 | ok $t->_sg, "Found blog entry"; 34 | is $t->_sg->title, 'modFoss', 'Title updated.'; 35 | is $t->_sg->url, 'https://modfoss.com/', 'URL updated.'; 36 | is $t->_sg->rss_url , 'https://modfoss.com/feed', 'RSS URL updated.'; 37 | is $t->_sg->tagline , 'Articles on technical matters.', 'Tagline updated.'; 38 | is $t->_sg->about , 'A technical blog.', 'About updated.'; 39 | })->stash_has( { authorization => [ 'setting:can_manage_blogs' ] } ); 40 | 41 | done_testing; -------------------------------------------------------------------------------- /Web/t/01_endpoints/03_blog/02_edit_new_blog/04_no_anonymous_edit.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # Test that a new blog submitted by an anonymous user cannot be edited by a 3 | # different anonymous user. 4 | use Mojo::Base '-signatures'; 5 | use BlogDB::Web::Test; 6 | 7 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 8 | 9 | # Post a new blog entry. 10 | $t->post_ok( '/blog/new', 11 | form => { 12 | url => 'https://modfoss.com/', 13 | })->code_block( sub { 14 | my ( $t ) = @_; 15 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 16 | ok $t->_sg, "Created blog entry."; 17 | }); 18 | 19 | my $blog_id = $t->_sg->id; 20 | 21 | # New Session. 22 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 23 | 24 | $t->post_ok( "/blog/new/$blog_id", form => { 25 | title => 'modFoss', 26 | url => 'https://modfoss.com/', 27 | rss_url => 'https://modfoss.com/feed', 28 | tagline => 'Articles on technical matters.', 29 | about => 'A technical blog.' 30 | })->code_block( sub { 31 | my ( $t ) = @_; 32 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 33 | ok $t->_sg, "Found blog entry"; 34 | is $t->_sg->title, undef, 'Title still the same'; 35 | is $t->_sg->url, 'https://modfoss.com/', 'URL the same.'; 36 | is $t->_sg->rss_url , undef, 'RSS URL still the same.'; 37 | is $t->_sg->tagline , undef, 'Tagline still the same.'; 38 | is $t->_sg->about , undef, 'About still the same.'; 39 | })->stash_has( { errors => [ 'Not Authorized.' ] } ); 40 | 41 | done_testing; -------------------------------------------------------------------------------- /Web/t/01_endpoints/03_blog/02_edit_new_blog/05_no_alt_user_edit.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # Test that a new blog submitted by a logged in user cannot be edited by another user. 3 | use Mojo::Base '-signatures'; 4 | use BlogDB::Web::Test; 5 | 6 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 7 | 8 | # Post a new blog as a logged in user, ensure that it exists, make sure that the submitter id matches. 9 | $t->create_user->post_ok( '/blog/new', 10 | form => { 11 | url => 'https://modfoss.com/', 12 | })->code_block( sub { 13 | my ( $t ) = @_; 14 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 15 | ok $t->_sg, "Created blog entry."; 16 | is $t->_sg->submitter_id, $t->stash->{person}->id, 'Owned by the current user.'; 17 | is $t->_sg->edit_token, undef, 'No edit token for a user-submitted blog.'; 18 | }); 19 | 20 | my $blog_id = $t->_sg->id; 21 | 22 | # New Session. 23 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 24 | 25 | $t->create_user->post_ok( "/blog/new/$blog_id", form => { 26 | title => 'modFoss', 27 | url => 'https://modfoss.com/', 28 | rss_url => 'https://modfoss.com/feed', 29 | tagline => 'Articles on technical matters.', 30 | about => 'A technical blog.' 31 | })->code_block( sub { 32 | my ( $t ) = @_; 33 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 34 | ok $t->_sg, "Found blog entry"; 35 | is $t->_sg->title, undef, 'Title still the same'; 36 | is $t->_sg->url, 'https://modfoss.com/', 'URL the same.'; 37 | is $t->_sg->rss_url , undef, 'RSS URL still the same.'; 38 | is $t->_sg->tagline , undef, 'Tagline still the same.'; 39 | is $t->_sg->about , undef, 'About still the same.'; 40 | })->stash_has( { errors => [ 'Not Authorized.' ] } ); 41 | 42 | done_testing; -------------------------------------------------------------------------------- /Web/t/01_endpoints/03_blog/03_publish_new_blog/01_with_can_manage_blogs.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base '-signatures'; 3 | use BlogDB::Web::Test; 4 | 5 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 6 | 7 | # Post a new blog as a logged in user. 8 | $t->create_user->post_ok( '/blog/new', 9 | form => { 10 | url => 'https://modfoss.com/', 11 | })->code_block( sub { 12 | my ( $t ) = @_; 13 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 14 | ok $t->_sg, "Created blog entry."; 15 | }); 16 | 17 | my $blog_id = $t->_sg->id; 18 | 19 | # New Session, update the blog as a can_manage_blogs user.. 20 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 21 | 22 | $t->create_user({ can_manage_blogs => 1 })->post_ok( "/blog/new/$blog_id", form => { 23 | title => 'modFoss', 24 | url => 'https://modfoss.com/', 25 | rss_url => 'https://modfoss.com/feed', 26 | tagline => 'Articles on technical matters.', 27 | about => 'A technical blog.' 28 | })->code_block( sub { 29 | my ( $t ) = @_; 30 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 31 | ok $t->_sg, "Found blog entry"; 32 | is $t->_sg->title, 'modFoss', 'Title updated.'; 33 | is $t->_sg->url, 'https://modfoss.com/', 'URL updated.'; 34 | is $t->_sg->rss_url , 'https://modfoss.com/feed', 'RSS URL updated.'; 35 | is $t->_sg->tagline , 'Articles on technical matters.', 'Tagline updated.'; 36 | is $t->_sg->about , 'A technical blog.', 'About updated.'; 37 | })->stash_has( { authorization => [ 'setting:can_manage_blogs' ] } ); 38 | 39 | # Now we publish the blog, we're still in the user account with can_manage_blogs 40 | $t->post_ok( "/blog/publish/$blog_id", form => {}) 41 | ->code_block( sub { 42 | my ( $t ) = @_; 43 | $t->_ss($t->app->db->resultset('Blog')->find( { url => 'https://modfoss.com/'})); 44 | ok $t->_sg, "Found published blog"; 45 | is $t->_sg->title, 'modFoss', 'Blog has correct title.'; 46 | 47 | 48 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 49 | is $t->_sg, undef, "Blog has been deleted from PendingBlogs."; 50 | }); 51 | 52 | done_testing; -------------------------------------------------------------------------------- /Web/t/01_endpoints/03_blog/03_publish_new_blog/02_disable_for_normal_user.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # Test to make sure that a normal user (w/o can_manage_blogs) cannot approve a blog. 3 | use Mojo::Base '-signatures'; 4 | use BlogDB::Web::Test; 5 | 6 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 7 | 8 | # Post a new blog as a logged in user. 9 | $t->create_user->post_ok( '/blog/new', 10 | form => { 11 | url => 'https://modfoss.com/', 12 | })->code_block( sub { 13 | my ( $t ) = @_; 14 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 15 | ok $t->_sg, "Created blog entry."; 16 | }); 17 | 18 | my $blog_id = $t->_sg->id; 19 | 20 | # Update the blog as the same user. 21 | $t->post_ok( "/blog/new/$blog_id", form => { 22 | title => 'modFoss', 23 | url => 'https://modfoss.com/', 24 | rss_url => 'https://modfoss.com/feed', 25 | tagline => 'Articles on technical matters.', 26 | about => 'A technical blog.' 27 | })->code_block( sub { 28 | my ( $t ) = @_; 29 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 30 | ok $t->_sg, "Found blog entry"; 31 | is $t->_sg->title, 'modFoss', 'Title updated.'; 32 | })->stash_has( { authorization => [ 'submitter' ] } ); 33 | 34 | # Now we try to publish the blog, it should fail for no user account. 35 | $t->post_ok( "/blog/publish/$blog_id", form => {}) 36 | ->stash_has( { errors => [ 'Not Authorized.' ] }); 37 | 38 | 39 | 40 | done_testing; -------------------------------------------------------------------------------- /Web/t/01_endpoints/03_blog/03_publish_new_blog/03_disable_for_anon_user.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # Test to make sure that an anonymous user cannot approve a blog. 3 | use Mojo::Base '-signatures'; 4 | use BlogDB::Web::Test; 5 | 6 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 7 | 8 | # Post a new blog as a logged in user. 9 | $t->create_user->post_ok( '/blog/new', 10 | form => { 11 | url => 'https://modfoss.com/', 12 | })->code_block( sub { 13 | my ( $t ) = @_; 14 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 15 | ok $t->_sg, "Created blog entry."; 16 | }); 17 | 18 | my $blog_id = $t->_sg->id; 19 | 20 | # Update the blog as the same user. 21 | $t->post_ok( "/blog/new/$blog_id", form => { 22 | title => 'modFoss', 23 | url => 'https://modfoss.com/', 24 | rss_url => 'https://modfoss.com/feed', 25 | tagline => 'Articles on technical matters.', 26 | about => 'A technical blog.' 27 | })->code_block( sub { 28 | my ( $t ) = @_; 29 | $t->_ss($t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})); 30 | ok $t->_sg, "Found blog entry"; 31 | is $t->_sg->title, 'modFoss', 'Title updated.'; 32 | })->stash_has( { authorization => [ 'submitter' ] } ); 33 | 34 | # Now we try to publish the blog, it should fail because no logged in user. 35 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 36 | $t->post_ok( "/blog/publish/$blog_id", form => {}) 37 | ->stash_has( { errors => [ 'Login required.']}, 'Thrown out for no login.'); 38 | 39 | done_testing; -------------------------------------------------------------------------------- /Web/t/01_endpoints/03_blog/04_view_blog.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base '-signatures'; 3 | use BlogDB::Web::Test; 4 | 5 | # Create and publish the modFoss blog. 6 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 7 | 8 | my ($blog_id, $slug); 9 | $t->create_user({can_manage_blogs => 1})->post_ok( '/blog/new', 10 | form => { url => 'https://modfoss.com/', 11 | })->code_block( sub { 12 | $blog_id = $t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})->id; 13 | })->post_ok( "/blog/new/$blog_id", form => { 14 | title => 'modFoss', 15 | url => 'https://modfoss.com/', 16 | rss_url => 'https://modfoss.com/feed', 17 | tagline => 'Articles on technical matters.', 18 | about => 'A technical blog.' 19 | })->post_ok( "/blog/publish/$blog_id", form => { 20 | 21 | })->code_block( sub { 22 | $slug = $t->app->db->resultset('Blog')->find( { url => 'https://modfoss.com/'})->slug; 23 | }); 24 | 25 | # View the blog as an anonymous user. 26 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 27 | 28 | $t->get_ok( "/blog/v/$slug" )->code_block( sub { 29 | my $t = shift; 30 | ok $t->stash->{blog}; 31 | is $t->stash->{person}, undef, 'No person object for anon.'; 32 | is $t->stash->{blog}->title, 'modFoss', 'Blog object found.'; 33 | }); 34 | 35 | # View the blog as a logged in user. 36 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 37 | 38 | $t->create_user->get_ok( "/blog/v/$slug" )->code_block( sub { 39 | my $t = shift; 40 | ok $t->stash->{blog}; 41 | ok $t->stash->{person}, 'Person object for logged in user.'; 42 | is $t->stash->{blog}->title, 'modFoss', 'Blog object found.'; 43 | }); 44 | 45 | done_testing; -------------------------------------------------------------------------------- /Web/t/01_endpoints/03_blog/05_edit_blog.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base '-signatures'; 3 | use BlogDB::Web::Test; 4 | 5 | # Create and publish the modFoss blog. 6 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 7 | 8 | my ($blog_id, $slug); 9 | $t->create_user({can_manage_blogs => 1})->post_ok( '/blog/new', 10 | form => { url => 'https://modfoss.com/', 11 | })->code_block( sub { 12 | $blog_id = $t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})->id; 13 | })->post_ok( "/blog/new/$blog_id", form => { 14 | title => 'modFoss', 15 | url => 'https://modfoss.com/', 16 | rss_url => 'https://modfoss.com/feed', 17 | tagline => 'Articles on technical matters.', 18 | about => 'A technical blog.' 19 | })->post_ok( "/blog/publish/$blog_id", form => { 20 | })->code_block( sub { 21 | $slug = $t->app->db->resultset('Blog')->find( { url => 'https://modfoss.com/'})->slug; 22 | }); 23 | 24 | # Trying to edit the blog as an anonymous user doesn't work. 25 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 26 | 27 | $t->get_ok( "/blog/e/$slug" )->stash_has( { 28 | errors => [ 'Login required.' ] 29 | }, 'Cannot view edit blog page without login.' ); 30 | 31 | $t->post_ok( "/blog/e/$slug" )->stash_has( { 32 | errors => [ 'Login required.' ] 33 | }, 'Cannot view edit blog page without login.' ); 34 | 35 | # Trying to edit the blog as a logged in user doesn't work, 36 | # without the can_manage_blogs permission. 37 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 38 | 39 | $t->create_user->get_ok( "/blog/e/$slug" )->stash_has( { 40 | errors => [ 'Not Authorized.' ] 41 | }, 'Cannot view edit blog page without can_manage_blogs.' ); 42 | 43 | $t->post_ok( "/blog/e/$slug" )->stash_has( { 44 | errors => [ 'Not Authorized.' ] 45 | }, 'Cannot post to edit blog page without can_manage_blogs.' ); 46 | 47 | # Editing the blog with can_manage_blogs does work. 48 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 49 | 50 | $t->create_user({ can_manage_blogs => 1 })->get_ok( "/blog/e/$slug" )->code_block( sub { 51 | ok $t->stash->{blog}, "Have blog object."; 52 | is $t->stash->{form_title}, 'modFoss', 'Form stash works.'; 53 | }); 54 | 55 | $t->post_ok( "/blog/e/$slug", form => { 56 | title => 'modFoss Blog', 57 | url => 'http://modfoss.com/', 58 | rss_url => 'http://modfoss.com/feed', 59 | tagline => 'Technical Matters under Articles', 60 | about => 'Blog Technical, A?', 61 | })->code_block( sub { 62 | $t->_ss($t->app->db->resultset('Blog')->find( { url => 'https://modfoss.com/'})); 63 | is $t->_sg, undef, 'Blog cannot be found by the old URL.'; 64 | $t->_ss($t->app->db->resultset('Blog')->find( { url => 'http://modfoss.com/'})); 65 | ok $t->_sg, 'Blog is found by the new URL now.'; 66 | is $t->_sg->title, 'modFoss Blog', 'The blog updated.'; 67 | 68 | }); 69 | 70 | done_testing; -------------------------------------------------------------------------------- /Web/t/01_endpoints/03_blog/06_follow_blog.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base '-signatures'; 3 | use BlogDB::Web::Test; 4 | 5 | # Create and publish the modFoss blog. 6 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 7 | 8 | my ($blog_id, $slug); 9 | $t->create_user({can_manage_blogs => 1})->post_ok( '/blog/new', 10 | form => { url => 'https://modfoss.com/', 11 | })->code_block( sub { 12 | $blog_id = $t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})->id; 13 | })->post_ok( "/blog/new/$blog_id", form => { 14 | title => 'modFoss', 15 | url => 'https://modfoss.com/', 16 | rss_url => 'https://modfoss.com/feed', 17 | tagline => 'Articles on technical matters.', 18 | about => 'A technical blog.' 19 | })->post_ok( "/blog/publish/$blog_id", form => { 20 | })->code_block( sub { 21 | $slug = $t->app->db->resultset('Blog')->find( { url => 'https://modfoss.com/'})->slug; 22 | }); 23 | 24 | # User who isn't logged in cannot follow a blog. 25 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 26 | $t->post_ok( '/blog/follow', form => { blog_id => $blog_id }) 27 | ->stash_has( { errors => [ 'Login required.']}, 'Login required to follow blogs.'); 28 | 29 | # Regular user can follow a blog. 30 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 31 | $t->create_user->post_ok( '/blog/follow', form => { blog_id => $blog_id }) 32 | ->code_block( sub { 33 | my $t = shift; 34 | my $blogs = $t->stash->{person}->get_followed_blogs; 35 | is $blogs->[0]->id, $blog_id, "A logged in user can follow a blog."; 36 | }); 37 | 38 | done_testing; -------------------------------------------------------------------------------- /Web/t/01_endpoints/03_blog/07_comment_on_blog.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base '-signatures'; 3 | use BlogDB::Web::Test; 4 | 5 | # Create and publish the modFoss blog. 6 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 7 | 8 | my ($blog_id, $slug); 9 | $t->create_user({can_manage_blogs => 1})->post_ok( '/blog/new', 10 | form => { url => 'https://modfoss.com/', 11 | })->code_block( sub { 12 | $blog_id = $t->app->db->resultset('PendingBlog')->find( { url => 'https://modfoss.com/'})->id; 13 | })->post_ok( "/blog/new/$blog_id", form => { 14 | title => 'modFoss', 15 | url => 'https://modfoss.com/', 16 | rss_url => 'https://modfoss.com/feed', 17 | tagline => 'Articles on technical matters.', 18 | about => 'A technical blog.' 19 | })->post_ok( "/blog/publish/$blog_id", form => { 20 | })->code_block( sub { 21 | $slug = $t->app->db->resultset('Blog')->find( { url => 'https://modfoss.com/'})->slug; 22 | }); 23 | 24 | # User who isn't logged in cannot comment on a blog.. 25 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 26 | $t->post_ok( '/blog/comment', form => { 27 | blog_id => $blog_id, 28 | message => 'First Comment', 29 | rev_pos => 1, 30 | })->stash_has( { errors => [ 'Login required.']}, 'Login required to post comment.'); 31 | 32 | # User can post a comment. 33 | # They (or another user) can then reply to comments. 34 | # Create a comment, then reply to it, verify both comments show up. 35 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 36 | $t->create_user->post_ok( '/blog/comment', form => { 37 | blog_id => $blog_id, 38 | message => 'First Comment', 39 | rev_pos => 1, 40 | })->code_block( sub { 41 | my $t = shift; 42 | $t->_ss($t->app->db->resultset('Message')->find($t->stash->{created_comment_id})); 43 | ok $t->_sg, 'Got comment object.'; 44 | is $t->_sg->content, 'First Comment', 'Comment object has correct message.'; 45 | $t->_ss( $t->_sg->id ); 46 | })->post_ok( '/blog/comment', form => { 47 | blog_id => $blog_id, 48 | message => 'Child Comment', 49 | rev_pos => 1, 50 | parent_id => $t->_sg, 51 | })->code_block( sub { 52 | $t->_ss($t->app->db->resultset('Message')->find($t->_sg)); 53 | ok $t->_sg, 'Got comment object.'; 54 | is $t->_sg->content, 'First Comment', 'Comment object has correct message.'; 55 | $t->_ss( @{$t->_sg->get_children} ); 56 | ok $t->_sg, 'Got comment object\'s child.'; 57 | is $t->_sg->content, 'Child Comment', 'Comment object has correct message.'; 58 | }); 59 | 60 | done_testing; -------------------------------------------------------------------------------- /Web/t/02_html/01_index/00_exists.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | #== 3 | # Test to ensure / exists and has the correct title. 4 | #== 5 | use Mojo::Base '-signatures'; 6 | use BlogDB::Web::Test; 7 | 8 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 9 | 10 | $t->get_ok( '/') 11 | ->status_is( 200 ) 12 | ->text_is('title', 'BlogDB - Homepage'); 13 | 14 | done_testing; -------------------------------------------------------------------------------- /Web/t/02_html/02_register/00_exists.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base '-signatures'; 3 | use BlogDB::Web::Test; 4 | 5 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 6 | 7 | $t->get_ok( '/register') 8 | ->status_is( 200 ) 9 | ->text_is('title', 'BlogDB - Register'); 10 | 11 | done_testing; -------------------------------------------------------------------------------- /Web/t/02_html/03_forgot/00_exists.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | #== 3 | # Test to ensure /forgot exists. 4 | #== 5 | use Mojo::Base '-signatures'; 6 | use BlogDB::Web::Test; 7 | 8 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 9 | 10 | $t->get_ok( '/forgot') 11 | ->status_is( 200 ) 12 | ->text_is('title', 'BlogDB - Forgot Password'); 13 | 14 | done_testing; -------------------------------------------------------------------------------- /Web/t/02_html/03_forgot/01_var_token/00_exists.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | #== 3 | # Test to ensure that /forgot/$token exists and has the correct title. 4 | # 5 | # This page only exists after an esblished user account submits a valid 6 | # password reset request to /forgot, so this code will do that as well. 7 | #== 8 | use Mojo::Base '-signatures'; 9 | use BlogDB::Web::Test; 10 | 11 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 12 | 13 | # Try creating a valid account, ensure it exists in the DB. 14 | $t->post_ok( '/register', form => { 15 | username => 'fred', 16 | email => 'fred@blog.com', 17 | password => 'SuperSecure', 18 | confirm => 'SuperSecure', 19 | })->status_is( 302 20 | )->code_block( sub { 21 | is( scalar(@{shift->stash->{errors}}), 0, 'No errors' ); 22 | })->code_block( sub { 23 | is( shift->app->db->resultset('Person')->search( { username => 'fred'})->count, 1, 'User created.'); 24 | })->get_ok( '/' 25 | )->code_block( sub { 26 | is(shift->stash->{person}->username, 'fred', 'Got the fred after login...'); 27 | })->stash_has( { blogs => [ ]}, "Blog entry array ref exists."); 28 | 29 | # New Session, verify it isn't logged in. 30 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 31 | 32 | $t->get_ok( '/' )->code_block( sub { is(shift->stash->{person}, undef, "Not logged in.") }); 33 | 34 | $t->post_ok( '/forgot', form => { username => 'fred' }) 35 | ->status_is( 200 ) 36 | ->stash_has( { success => 1 }, "Finished setting token." ) 37 | ->code_block( sub { 38 | my $t = shift; 39 | ok my $fred = $t->app->db->resultset('Person')->find( { username => 'fred' }), "Found fred in DB."; 40 | ok my $reset_token = $fred->search_related('password_tokens')->first->token, "Found reset token in DB."; 41 | $t->stash( { %{$t->stash}, token => $reset_token } ) 42 | }); 43 | 44 | # Extract the reset token from the stash. 45 | my $reset_token = $t->stash->{token}; 46 | 47 | # Finally, verify the page exists now. 48 | $t->get_ok( "/forgot/$reset_token") 49 | ->status_is( 200 ) 50 | ->text_is('title', 'BlogDB - Reset Password'); 51 | 52 | done_testing; -------------------------------------------------------------------------------- /Web/t/03_workflows/01_register/01_basic.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | #== 3 | # Register an account and confirm the user is logged into it. 4 | #== 5 | use Mojo::Base '-signatures'; 6 | use BlogDB::Web::Test; 7 | 8 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 9 | 10 | # Ensure we have a registration page with a valid form. 11 | $t->get_ok( '/register') 12 | ->element_exists( 'form[name="register"]') 13 | ->element_exists( 'input[data-form="register"][name="username"]') 14 | ->element_exists( 'input[data-form="register"][name="email"]') 15 | ->element_exists( 'input[data-form="register"][name="password"]') 16 | ->element_exists( 'input[data-form="register"][name="confirm"]'); 17 | 18 | # Submit the form, confirm the redirect to the home page, and that we are shown as logged in. 19 | $t->post_ok( '/register', form => { 20 | username => 'fred', 21 | email => 'fred@blog.com', 22 | password => 'SuperSecure', 23 | confirm => 'SuperSecure', 24 | })->status_is( 302 ) # Redirect status set 25 | ->header_is( 'location', '/') # Redirect is to home page 26 | ->get_ok('/') 27 | ->element_exists( 'a[href="/user/settings"]' ) # Settings button - we're logged in. 28 | ->element_exists_not( 'form[name="login"]'); # No login form - we're logged in. 29 | 30 | done_testing(); -------------------------------------------------------------------------------- /Web/t/03_workflows/01_register/02_bad_confirmation.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | #== 3 | # Confirm mismatched passwords on the registration don't work. 4 | #== 5 | use Mojo::Base '-signatures'; 6 | use BlogDB::Web::Test; 7 | 8 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 9 | 10 | # Submit the form and confirm we are told the errors and not allowed to proceed. 11 | $t->post_ok( '/register', form => { 12 | username => 'fred', 13 | email => 'fred@blog.com', 14 | password => 'SuperSecure', 15 | confirm => 'NotSuperSecure', 16 | })->status_is( 200 ) 17 | ->content_like( qr|There were errors with your request| ) 18 | ->content_like( qr|Password & Confirmation must match| ); 19 | 20 | done_testing(); -------------------------------------------------------------------------------- /Web/t/03_workflows/01_register/03_no_password.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | #== 3 | # Confirm a lack of password field results in an error. 4 | #== 5 | use Mojo::Base '-signatures'; 6 | use BlogDB::Web::Test; 7 | 8 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 9 | 10 | # Submit the form and confirm we are told the errors and not allowed to proceed. 11 | $t->post_ok( '/register', form => { 12 | username => 'fred', 13 | email => 'fred@blog.com', 14 | confirm => 'NotSuperSecure', 15 | })->status_is( 200 ) 16 | ->content_like( qr|There were errors with your request| ) 17 | ->content_like( qr|Password is required| ); 18 | 19 | done_testing(); -------------------------------------------------------------------------------- /Web/t/03_workflows/01_register/04_short_password.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | #== 3 | # Confirm a too-short password results in an error. 4 | #== 5 | use Mojo::Base '-signatures'; 6 | use BlogDB::Web::Test; 7 | 8 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 9 | 10 | # Submit the form and confirm we are told the errors and not allowed to proceed. 11 | $t->post_ok( '/register', form => { 12 | username => 'fred', 13 | email => 'fred@blog.com', 14 | password => 'Short', 15 | confirm => 'Short', 16 | })->status_is( 200 ) 17 | ->content_like( qr|There were errors with your request| ) 18 | ->content_like( qr|Password must be at least 7 chars| ); 19 | 20 | done_testing(); -------------------------------------------------------------------------------- /Web/t/03_workflows/01_register/05_no_confirmation.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | #== 3 | # Confirm a missing confirmation field results in an error. 4 | #== 5 | use Mojo::Base '-signatures'; 6 | use BlogDB::Web::Test; 7 | 8 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 9 | 10 | # Submit the form and confirm we are told the errors and not allowed to proceed. 11 | $t->post_ok( '/register', form => { 12 | username => 'fred', 13 | email => 'fred@blog.com', 14 | password => 'SuperSecure', 15 | })->status_is( 200 ) 16 | ->content_like( qr|There were errors with your request| ) 17 | ->content_like( qr|Confirm password is required| ); 18 | 19 | done_testing(); -------------------------------------------------------------------------------- /Web/t/03_workflows/01_register/06_username_taken.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | #== 3 | # Confirm a user cannot register an already-registered username. 4 | #== 5 | use Mojo::Base '-signatures'; 6 | use BlogDB::Web::Test; 7 | 8 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 9 | 10 | # Register a valid user account for fred. 11 | $t->post_ok( '/register', form => { 12 | username => 'fred', 13 | email => 'fred@blog.com', 14 | password => 'SuperSecure', 15 | confirm => 'SuperSecure', 16 | })->status_is( 302 ); 17 | 18 | # New Session 19 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 20 | 21 | # Submit the form and confirm we are told the errors and not allowed to proceed. 22 | $t->post_ok( '/register', form => { 23 | username => 'fred', 24 | email => 'fred.two@blog.com', 25 | password => 'SuperSecure', 26 | confirm => 'SuperSecure', 27 | })->status_is( 200 ) 28 | ->content_like( qr|There were errors with your request| ) 29 | ->content_like( qr|This username is already in use| ); 30 | 31 | done_testing(); -------------------------------------------------------------------------------- /Web/t/03_workflows/01_register/07_email_taken.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | #== 3 | # Confirm a user cannot register an already-registered email address. 4 | #== 5 | use Mojo::Base '-signatures'; 6 | use BlogDB::Web::Test; 7 | 8 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 9 | 10 | # Register a valid user account for fred. 11 | $t->post_ok( '/register', form => { 12 | username => 'fred', 13 | email => 'fred@blog.com', 14 | password => 'SuperSecure', 15 | confirm => 'SuperSecure', 16 | })->status_is( 302 ); 17 | 18 | # New Session 19 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 20 | 21 | # Submit the form and confirm we are told the errors and not allowed to proceed. 22 | $t->post_ok( '/register', form => { 23 | username => 'fred_two', 24 | email => 'fred@blog.com', 25 | password => 'SuperSecure', 26 | confirm => 'SuperSecure', 27 | })->status_is( 200 ) 28 | ->content_like( qr|There were errors with your request| ) 29 | ->content_like( qr|This email address is already registered| ); 30 | 31 | done_testing(); -------------------------------------------------------------------------------- /Web/t/03_workflows/02_login/01_basic.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | #== 3 | # Test to confirm we can log into an account after it has been created. 4 | #== 5 | use Mojo::Base '-signatures'; 6 | use BlogDB::Web::Test; 7 | 8 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 9 | 10 | # Try creating a valid account, ensure it exists in the DB. 11 | $t->post_ok( '/register', form => { 12 | username => 'fred', 13 | email => 'fred@blog.com', 14 | password => 'SuperSecure', 15 | confirm => 'SuperSecure', 16 | })->status_is( 302 ); 17 | 18 | # Create new session so we are not logged in. 19 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 20 | 21 | # Ensure that the login form exists on the home page. 22 | $t->get_ok( '/') 23 | ->element_exists( 'form[name="login"]') 24 | ->element_exists( 'input[type="hidden"][name="return_url"]') 25 | ->element_exists( 'input[name="username"]') 26 | ->element_exists( 'input[name="password"]') 27 | ->element_exists_not( 'a[href="/user/settings"]' ); 28 | 29 | # Confirm login behavior. 30 | $t->post_ok( '/login', form => { username => 'fred', password => 'SuperSecure', return_url => '/'}) 31 | ->status_is( 302 ) # Redirect status is set 32 | ->header_is( 'location', '/') # Redirect location is what return_url was 33 | ->get_ok('/') # Manually go to / 34 | ->element_exists( 'a[href="/user/settings"]' ) # Confirm the user settings button showing we're logged in. 35 | ->element_exists_not( 'form[name="login"]'); # Confirm the login form is no longer present. 36 | 37 | done_testing; -------------------------------------------------------------------------------- /Web/t/03_workflows/02_login/02_no_account.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | #== 3 | # Confirm a login with an invalid account doesn't work. 4 | #== 5 | use Mojo::Base '-signatures'; 6 | use BlogDB::Web::Test; 7 | 8 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 9 | 10 | # Complete login and confirm we do not login 11 | $t->post_ok( '/login', form => { username => 'fred', password => 'SuperSecure', return_url => '/'}) 12 | ->status_is( 302 ) 13 | ->header_is( 'location', '/') 14 | ->get_ok('/') 15 | ->element_exists( 'form[name="login"]'); # After login attempt, we still have login form. 16 | 17 | done_testing; -------------------------------------------------------------------------------- /Web/t/03_workflows/02_login/03_wrong_password.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | #== 3 | # Confirm a login with an invalid password on a created account. 4 | #== 5 | use Mojo::Base '-signatures'; 6 | use BlogDB::Web::Test; 7 | 8 | my $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 9 | 10 | # Try creating a valid account, ensure it exists in the DB. 11 | $t->post_ok( '/register', form => { 12 | username => 'fred', 13 | email => 'fred@blog.com', 14 | password => 'SuperSecure', 15 | confirm => 'SuperSecure', 16 | })->status_is( 302 ); 17 | 18 | # Create new session so we are not logged in. 19 | $t = Test::Mojo::BlogDB->new('BlogDB::Web'); 20 | 21 | # Complete login and confirm we do not gain access to the account with an incorrect password. 22 | $t->post_ok( '/login', form => { username => 'fred', password => 'VeryWrongPassword', return_url => '/'}) 23 | ->status_is( 302 ) 24 | ->header_is( 'location', '/') 25 | ->get_ok('/') 26 | ->element_exists_not( 'a[href="/user/settings"]' ) # There is no settings button. 27 | ->element_exists( 'form[name="login"]'); # We still have a login form. 28 | 29 | done_testing; -------------------------------------------------------------------------------- /Web/templates/default/_/form/input.tx: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
[% $help %]
6 |
7 | 8 | -------------------------------------------------------------------------------- /Web/templates/default/_/layout.tx: -------------------------------------------------------------------------------- 1 | %% my $gear_icon = mark_raw(' '); 2 | 3 | %% my $door_icon = mark_raw(''); 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | 26 | 27 | [% $title ? "BlogDB - " ~ $title : "BlogDB" %] 28 | 29 | 30 | 65 | 66 |
67 | 68 | %% block panel -> {} 69 | 70 |
71 | 72 | 73 | 74 | 75 | 76 | 77 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /Web/templates/default/blog/_comment.tx: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | u/[% $comment.author.username %] 5 | [% $comment.time_ago %] 6 |
7 |
8 | %% $comment.content 9 |
10 |
11 | 12 | [Permlink] 13 |
14 |
15 | 16 | 17 |
18 | 19 | 20 |
21 | 22 | 23 |
24 |
25 |
26 | %% for $comment.get_children -> $child_comment { 27 |
28 | %% include "/default/blog/_comment.tx" { comment => $child_comment }; 29 |
30 | %% } 31 |
-------------------------------------------------------------------------------- /Web/templates/default/blog/edit.html.tx: -------------------------------------------------------------------------------- 1 | 2 | %% cascade default::_::layout { title => 'Edit ' ~ $blog_url, 3 | %% 4 | %% } 5 | 6 | %% override panel -> { 7 |

[% $blog.title %]

8 | 9 | 10 |
11 |
12 |
13 | 14 |
15 |
16 |
17 |
18 |
19 | 20 |
21 | 22 |
23 | %% if ( $errors.size() ) { 24 | 32 | %% } 33 |
34 |
35 | 36 |
37 |
38 |
39 | 40 | %% include 'default/_/form/input.tx' { type => 'text', name => 'title', 41 | %% title => 'Title', 42 | %% help => 'The title of the blog', 43 | %% value => $form_title, 44 | %% }; 45 | 46 | %% include 'default/_/form/input.tx' { type => 'text', name => 'tagline', 47 | %% title => 'Tagline', 48 | %% help => 'The tagline of the blog.', 49 | %% value => $form_tagline, 50 | %% }; 51 | 52 | 53 | 54 | 55 |
56 | 57 |
58 | 59 | 60 | 61 |
62 | 63 | %% for $tags -> $tag { 64 |
65 | 66 | 67 |
68 | %% } 69 |
70 | %% include 'default/_/form/input.tx' { type => 'text', name => 'url', 71 | %% title => 'Homepage URL', 72 | %% help => 'The url of the blog', 73 | %% value => $form_url, 74 | %% }; 75 | 76 | %% include 'default/_/form/input.tx' { type => 'text', name => 'rss_url', 77 | %% title => 'RSS URL', 78 | %% help => 'A URL to an RSS feed for the blog.', 79 | %% value => $form_rss_url, 80 | %% }; 81 | 82 | 83 | 84 |
85 |
86 |
87 | 88 |
89 |
90 |
91 |

Recent Posts

92 | %% for $blog.posts -> $post { 93 | [% $post.title %] 94 | %% } 95 |
96 | 97 | %% } 98 | -------------------------------------------------------------------------------- /Web/templates/default/blog/item.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade default::_::layout { title => $blog.title, 2 | %% 3 | %% } 4 | 5 | %% override panel -> { 6 | 7 | 14 | 15 | 16 |
17 |
18 |

[% $blog.title %]

19 |

[% $blog.tagline %]

20 |
21 |
22 | %% if ( $person ) { 23 | %% if ( $person.is_following_blog($blog.id) ) { 24 |
25 | 26 | 27 |
28 | %% } else { 29 |
30 | 31 | 32 |
33 | %% } 34 | %% } 35 |
36 |
37 | 38 |
39 |
40 |

[% $blog.about %]

41 |

X readers follow

42 | 43 | %% for $blog.tags -> $tag { 44 | [% $tag.name %] 45 | %% } 46 | 47 |
48 |
49 | 50 |
51 |
52 | 53 | 61 | 62 |
63 | 64 | 65 | 66 | 67 |
68 | 69 | 70 |
71 | 72 |
73 | 74 | 75 |
76 | 77 | 78 |
79 | 80 |
81 |

Recent Posts

82 | %% for $blog.posts -> $post { 83 | [% $post.title %] 84 | %% } 85 |
86 | 87 |
88 | 89 | %% for $blog.get_comments -> $comment { 90 | %% include "/default/blog/_comment.tx" { comment => $comment }; 91 | %% } 92 | 93 | 94 | %% } -------------------------------------------------------------------------------- /Web/templates/default/blog/new/index.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade default::_::layout { title => 'List Pending Blogs', 2 | %% 3 | %% } 4 | 5 | %% override panel -> { 6 | 7 | %% for $blogs -> $blog { 8 |
9 | Screenshot 10 |
11 |
[% $blog.title %]
12 |

13 | [% $blog.tagline %]
14 | About: [% $blog.about %]
15 |

16 | View Edit Page 17 |
18 |
19 | 20 | %% } 21 | 22 | %% } -------------------------------------------------------------------------------- /Web/templates/default/blog/new/item.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade default::_::layout { title => 'Edit ' ~ $blog_url, 2 | %% 3 | %% } 4 | 5 | %% override panel -> { 6 |

[% $blog.title %]

7 | 8 | 9 |
10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 |
18 | 19 |
20 | 21 |
22 | %% if ( $errors.size() ) { 23 | 31 | %% } 32 |
33 |
34 | 35 |
36 |
37 |
38 | 39 | %% include 'default/_/form/input.tx' { type => 'text', name => 'title', 40 | %% title => 'Title', 41 | %% help => 'The title of the blog', 42 | %% value => $form_title, 43 | %% }; 44 | 45 | %% include 'default/_/form/input.tx' { type => 'text', name => 'tagline', 46 | %% title => 'Tagline', 47 | %% help => 'The tagline of the blog.', 48 | %% value => $form_tagline, 49 | %% }; 50 | 51 | 52 | 53 | 54 |
55 | 56 |
57 | 58 | 59 | 60 |
61 | 62 | %% for $tags -> $tag { 63 |
64 | 65 | 66 |
67 | %% } 68 |
69 | %% include 'default/_/form/input.tx' { type => 'text', name => 'url', 70 | %% title => 'Homepage URL', 71 | %% help => 'The url of the blog', 72 | %% value => $form_url, 73 | %% }; 74 | 75 | %% include 'default/_/form/input.tx' { type => 'text', name => 'rss_url', 76 | %% title => 'RSS URL', 77 | %% help => 'A URL to an RSS feed for the blog.', 78 | %% value => $form_rss_url, 79 | %% }; 80 | 81 | 82 | 83 |
84 |
85 |
86 | 87 |
88 |
89 |
90 |

Recent Posts

91 | %% for $blog.posts -> $post { 92 | [% $post.title %] 93 | %% } 94 |
95 | 96 | %% } 97 | -------------------------------------------------------------------------------- /Web/templates/default/forgot.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade default::_::layout { title => 'Forgot Password', 2 | %% 3 | %% } 4 | 5 | %% override panel -> { 6 |
7 | 8 |
9 | %% if ( $errors.size() ) { 10 | 18 | %% } 19 |
20 | 21 | 22 |
23 |
24 | %% include 'default/_/form/input.tx' { type => 'text', name => 'username', 25 | %% title => 'Username (or email)', 26 | %% help => 'The username or email address you signed up with.', 27 | %% value => $form_username, 28 | %% }; 29 | 30 | 31 | 32 |
33 |
34 | %% } 35 | -------------------------------------------------------------------------------- /Web/templates/default/index.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade default::_::layout { title => 'Home', 2 | %% 3 | %% } 4 | 5 | %% override panel -> { 6 | 7 | 8 |
9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 |
17 |
18 | 19 |

Followed Blogs

20 |
21 | %% for $person.get_followed_blogs -> $blog { 22 |
23 |
24 | Screenshot 25 |
26 |
[% $blog.title %]
27 |

28 | [% $blog.tagline %]
29 | About: [% $blog.about %]
30 |

31 |
32 | 35 |
36 |
37 | %% } 38 |
39 | 40 |

All Blogs

41 |
42 | %% for $blogs -> $blog { 43 |
44 |
45 | Screenshot 46 |
47 |
[% $blog.title %]
48 |

49 | [% $blog.tagline %]
50 | About: [% $blog.about %]
51 |

52 |
53 | 56 |
57 |
58 | %% } 59 |
60 | 61 | %% } 62 | -------------------------------------------------------------------------------- /Web/templates/default/new/index.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade default::_::layout { title => 'Edit ' ~ $blog_url, 2 | %% 3 | %% } 4 | 5 | %% override panel -> { 6 | 7 |

[% $blog_url %]

8 | 9 |
10 | 11 |
12 | %% if ( $errors.size() ) { 13 | 21 | %% } 22 |
23 |
24 | 25 |
26 |
27 |
28 | 29 | %% include 'default/_/form/input.tx' { type => 'text', name => 'title', 30 | %% title => 'Title', 31 | %% help => 'The title of the blog', 32 | %% value => $form_title, 33 | %% }; 34 | 35 | %% include 'default/_/form/input.tx' { type => 'text', name => 'tagline', 36 | %% title => 'Tagline', 37 | %% help => 'The tagline of the blog', 38 | %% value => $form_tagline, 39 | %% }; 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 |
48 | 49 | %% } 50 | -------------------------------------------------------------------------------- /Web/templates/default/register.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade default::_::layout { title => 'Register', 2 | %% 3 | %% } 4 | 5 | %% override panel -> { 6 |
7 | 8 |
9 | %% if ( $errors.size() ) { 10 | 18 | %% } 19 |
20 | 21 | 22 |
23 |
24 | %% include 'default/_/form/input.tx' { type => 'text', name => 'username', 25 | %% title => 'Username', 26 | %% help => 'Your username is unique, friends can follow you at /user/YourName', 27 | %% value => $form_username, 28 | %% }; 29 | 30 | %% include 'default/_/form/input.tx' { type => 'email', name => 'email', 31 | %% title => 'Email address', 32 | %% help => 'You will need to confirm your email address to post comments.', 33 | %% value => $form_email, 34 | %% }; 35 | 36 | %% include 'default/_/form/input.tx' { type => 'password', name => 'password', 37 | %% title => 'Password', 38 | %% help => 'You will need your password to login.', 39 | %% value => $form_password, 40 | %% }; 41 | 42 | %% include 'default/_/form/input.tx' { type => 'password', name => 'confirm', 43 | %% title => 'Confirm password', 44 | %% help => mark_raw('Just to annoy you be sure it is correct.'), 45 | %% value => $form_confirm, 46 | %% }; 47 | 48 | 49 | 50 |
51 |
52 | %% } 53 | -------------------------------------------------------------------------------- /Web/templates/default/reset.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade default::_::layout { title => 'Reset Password', 2 | %% 3 | %% } 4 | 5 | %% override panel -> { 6 |
7 | 8 |
9 | %% if ( $errors.size() ) { 10 | 18 | %% } 19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 | %% include 'default/_/form/input.tx' { type => 'password', name => 'password', 27 | %% title => 'Password', 28 | %% help => 'You will need your password to login.', 29 | %% value => $form_password, 30 | %% }; 31 | 32 | %% include 'default/_/form/input.tx' { type => 'password', name => 'confirm', 33 | %% title => 'Confirm password', 34 | %% help => mark_raw('Just to annoy you be sure it is correct.'), 35 | %% value => $form_confirm, 36 | %% }; 37 | 38 | 39 | 40 |
41 |
42 | %% } 43 | -------------------------------------------------------------------------------- /Web/templates/simple/_/_blog_card.tx: -------------------------------------------------------------------------------- 1 | 2 |
3 | ... 4 |
5 |
Last updated [% $blog.published_ago %]
6 |

[% $blog.title %]

7 |

[% $blog.tagline %]

8 | %% if ( $is_pending_link ) { 9 | Edit/Publish Blog → 10 | %% } else { 11 | View Entry → 12 | %% } 13 |
14 |
15 | -------------------------------------------------------------------------------- /Web/templates/simple/_/_blog_sidecard.tx: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 |
7 |
8 |
9 |
Last updated [% $blog.published_ago %]
10 |

[% $blog.title %]

11 |

[% $blog.tagline %]

12 | %% if ( $is_pending_link ) { 13 | Edit/Publish Blog → 14 | %% } else { 15 | View Entry → 16 | %% } 17 |
18 |
19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /Web/templates/simple/_/_blog_sidecard_new.tx: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 |
7 |
8 |
9 |

[% $blog.last_post.title %]

10 | 11 |
12 | Posted [% $blog.published_ago %]. 13 |
14 | 15 |

16 | See [% $blog.post_count %] more posts from 17 | [% $blog.title %] 18 | [% $blog.tagline %] 19 |

20 |
21 |
22 |
23 |
24 | 25 | -------------------------------------------------------------------------------- /Web/templates/simple/_/_entry_card.tx: -------------------------------------------------------------------------------- 1 | 2 |
3 | [% $entry.title %] 4 |
5 | 6 | Posted [% $entry.published_ago %] 7 | | 8 | [% $entry.blog.title %] 9 | %% if ( $entry.blog.tags.size() ) { 10 | | 11 | %% for $entry.blog.tags -> $tag { 12 | #[% $tag.name %] 13 | %% } 14 | %% } 15 | 16 | 17 |
18 | -------------------------------------------------------------------------------- /Web/templates/simple/_/form/input.tx: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
[% $help %]
6 |
7 | 8 | -------------------------------------------------------------------------------- /Web/templates/simple/about.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade simple::_::layout { title => 'About', 2 | %% 3 | %% } 4 | 5 | %% override panel -> { 6 | 7 |

BlogDB is intended to showcase a collection of blogs from around the Internet. Those who register accounts can follow blogs to get a feed of recent blog posts.

8 | 9 |

The initial design and programming was documented in a blog post, and the code is available on GitHub

10 | 11 |

You can get in touch with me at symkat@symkat.com

12 | 13 | %% } 14 | -------------------------------------------------------------------------------- /Web/templates/simple/blog/_comment.tx: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 |
6 | [% $comment.author.username %] 7 | [% $comment.time_ago %] 8 |
9 |
10 | %% $comment.content 11 |
12 |
13 | 14 | 15 | [Permlink] 16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 |
24 | 25 | %% if ( $person ) { 26 | 27 | %% } else { 28 | 29 | %% } 30 |
31 |
32 |
33 | %% for $comment.get_children -> $child_comment { 34 |
35 | %% include "/simple/blog/_comment.tx" { comment => $child_comment }; 36 |
37 | %% } 38 |
39 | -------------------------------------------------------------------------------- /Web/templates/simple/blog/edit.html.tx: -------------------------------------------------------------------------------- 1 | 2 | %% cascade simple::_::layout { title => 'Edit ' ~ $blog_url, 3 | %% 4 | %% } 5 | 6 | %% override blog_homepage_section -> { 7 |
8 |
Screenshot
9 |
10 | 11 |
...
12 |
13 |
14 |
15 | %% } 16 | 17 | %% override panel -> { 18 |

[% $blog.title %]

19 | 20 |
21 | 22 |
23 | %% if ( $errors.size() ) { 24 | 32 | %% } 33 |
34 |
35 | 36 |
37 |
38 |
39 | 40 | %% include 'simple/_/form/input.tx' { type => 'text', name => 'title', 41 | %% title => 'Title', 42 | %% help => 'The title of the blog', 43 | %% value => $form_title, 44 | %% }; 45 | 46 | %% include 'simple/_/form/input.tx' { type => 'text', name => 'tagline', 47 | %% title => 'Tagline', 48 | %% help => 'The tagline of the blog.', 49 | %% value => $form_tagline, 50 | %% }; 51 | 52 | 53 | 54 | 55 |
56 | 57 |
58 | 59 | 60 | 61 |
62 | 63 | %% for $tags -> $tag { 64 |
65 | 66 | 67 |
68 | %% } 69 |
70 | %% include 'simple/_/form/input.tx' { type => 'text', name => 'url', 71 | %% title => 'Homepage URL', 72 | %% help => 'The url of the blog', 73 | %% value => $form_url, 74 | %% }; 75 | 76 | %% include 'simple/_/form/input.tx' { type => 'text', name => 'rss_url', 77 | %% title => 'RSS URL', 78 | %% help => 'A URL to an RSS feed for the blog.', 79 | %% value => $form_rss_url, 80 | %% }; 81 | 82 | 83 | 84 |
85 |
86 |
87 |
88 |

Recent Posts

89 | %% for $blog.posts -> $post { 90 | [% $post.title %] 91 | %% } 92 |
93 | 94 | %% } 95 | -------------------------------------------------------------------------------- /Web/templates/simple/blog/index.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade simple::_::layout { title => 'Blog Listing', 2 | %% 3 | %% } 4 | 5 | %% override panel -> { 6 | 7 |
8 | 15 |
16 | 17 | 18 | %% for $blogs -> $blog { 19 |
20 | %% include 'simple/_/_blog_sidecard.tx' { blog => $blog }; 21 |
22 |
23 | %% } 24 | 25 | 33 | 34 | %%# 46 | 47 | %% } 48 | -------------------------------------------------------------------------------- /Web/templates/simple/blog/new/edit.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade simple::_::layout { title => 'Edit ' ~ $blog.url, 2 | %% hide_welcome_screen => 1, 3 | %% } 4 | 5 | %% override sidebar_card -> { 6 | %% if ( $person_permissions.can_manage_blogs ) { 7 |
8 |
Actions
9 |
10 |
11 | 12 |
13 | 14 |
15 | 16 |
17 | 18 |
19 |
20 | %% } 21 | %% } 22 | 23 | %% override blog_homepage_section -> { 24 |
25 |
Screenshot
26 |
27 | 28 |
...
29 |
30 |
31 |
32 | %% } 33 | 34 | %% override panel -> { 35 |

[% $blog.title %]

36 | 37 | 38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 |
46 | 47 |
48 | %% if ( $errors.size() ) { 49 | 57 | %% } 58 |
59 |
60 | 61 |
62 |
63 |
64 | 65 | %% include 'simple/_/form/input.tx' { type => 'text', name => 'title', 66 | %% title => 'Title', 67 | %% help => 'The title of the blog', 68 | %% value => $form_title, 69 | %% }; 70 | 71 | %% include 'simple/_/form/input.tx' { type => 'text', name => 'tagline', 72 | %% title => 'Tagline', 73 | %% help => 'The tagline of the blog.', 74 | %% value => $form_tagline, 75 | %% }; 76 | 77 | 78 | 79 | 80 |
81 | 82 |
83 | 84 | 85 | 86 |
87 | 88 | %% for $tags -> $tag { 89 |
90 | 91 | 92 |
93 | %% } 94 |
95 | %% include 'simple/_/form/input.tx' { type => 'text', name => 'url', 96 | %% title => 'Homepage URL', 97 | %% help => 'The url of the blog', 98 | %% value => $form_url, 99 | %% }; 100 | 101 | %% include 'simple/_/form/input.tx' { type => 'text', name => 'rss_url', 102 | %% title => 'RSS URL', 103 | %% help => 'A URL to an RSS feed for the blog.', 104 | %% value => $form_rss_url, 105 | %% }; 106 | 107 | 108 | 109 |
110 |
111 |
112 |
113 |

Recent Posts

114 | %% for $blog.posts -> $post { 115 | [% $post.title %] 116 | %% } 117 |
118 | 119 | %% } 120 | -------------------------------------------------------------------------------- /Web/templates/simple/blog/new/index.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade simple::_::layout { title => 'New Blogs', 2 | %% 3 | %% } 4 | 5 | %% override panel -> { 6 | %% for $blogs -> $blog { 7 | %% include 'simple/_/_blog_sidecard.tx' { blog => $blog, is_pending_link => 1 }; 8 | %% } 9 | %% } 10 | 11 | -------------------------------------------------------------------------------- /Web/templates/simple/blog/new/populating.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade simple::_::layout { title => 'Populating from' ~ $blog.url, 2 | %% hide_welcome_screen => 1, 3 | %% } 4 | 5 | %% override begin_html_head -> { 6 | 7 | %% } 8 | 9 | %% override panel -> { 10 |

Importing

11 | 12 |

This page will refresh until the blog data has been found, and then you will have a chance to edit the final submission.

13 | 14 | %% if ( $person_permissions.can_manage_blogs == 1 ) { 15 | Go To Edit Page Anyway 16 | %% } 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
Item GatheringTime Completed
Screenshot[% $has_img %]
Blog Data[% $has_inf %]
RSS Feed[% $has_rss %]
36 | %% } -------------------------------------------------------------------------------- /Web/templates/simple/feed/index.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade simple::_::layout { title => 'Feed', 2 | %% 3 | %% } 4 | 5 | %% override panel -> { 6 | 7 |
8 | 13 |
14 | 15 | 16 | %% for $entries -> $entry { 17 | %% include 'simple/_/_entry_card.tx' { entry => $entry }; 18 | %% } 19 | 20 | 28 | 29 | %% } 30 | -------------------------------------------------------------------------------- /Web/templates/simple/forgot.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade simple::_::layout { title => 'Forgot Password', 2 | %% 3 | %% } 4 | 5 | %% override panel -> { 6 |
7 | 8 |
9 | %% if ( $errors.size() ) { 10 | 18 | %% } 19 |
20 | 21 | 22 |
23 |
24 | %% include 'simple/_/form/input.tx' { type => 'text', name => 'username', 25 | %% title => 'Username (or email)', 26 | %% help => 'The username or email address you signed up with.', 27 | %% value => $form_username, 28 | %% }; 29 | 30 | 31 | 32 |
33 |
34 |
35 | %% } 36 | -------------------------------------------------------------------------------- /Web/templates/simple/index.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade simple::_::layout { title => 'Homepage', 2 | %% 3 | %% } 4 | 5 | %% override panel -> { 6 | 7 |
8 | 14 |
15 | 16 | 17 | %% for $blogs -> $blog { 18 |
19 | %% include 'simple/_/_blog_sidecard_new.tx' { blog => $blog }; 20 |
21 |
22 | %% } 23 | 24 | 32 | 33 | %% } 34 | -------------------------------------------------------------------------------- /Web/templates/simple/register.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade simple::_::layout { title => 'Register', 2 | %% 3 | %% } 4 | 5 | 6 | 7 | %% override panel -> { 8 |
9 | 10 |
11 | %% if ( $errors.size() ) { 12 | 20 | %% } 21 |
22 | 23 | 24 |
25 |
26 | %% include 'simple/_/form/input.tx' { type => 'text', name => 'username', 27 | %% form => 'register', 28 | %% title => 'Username', 29 | %% help => 'Your username is unique, friends can follow you at /user/YourName', 30 | %% value => $form_username, 31 | %% }; 32 | 33 | %% include 'simple/_/form/input.tx' { type => 'email', name => 'email', 34 | %% form => 'register', 35 | %% title => 'Email address', 36 | %% help => 'You will need to confirm your email address to post comments.', 37 | %% value => $form_email, 38 | %% }; 39 | 40 | %% include 'simple/_/form/input.tx' { type => 'password', name => 'password', 41 | %% form => 'register', 42 | %% title => 'Password', 43 | %% help => 'You will need your password to login.', 44 | %% value => $form_password, 45 | %% }; 46 | 47 | %% include 'simple/_/form/input.tx' { type => 'password', name => 'confirm', 48 | %% form => 'register', 49 | %% title => 'Confirm password', 50 | %% help => mark_raw('Just to annoy you be sure it is correct.'), 51 | %% value => $form_confirm, 52 | %% }; 53 | 54 | 55 | 56 |
57 |
58 |
59 | %% } -------------------------------------------------------------------------------- /Web/templates/simple/reset.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade simple::_::layout { title => 'Reset Password', 2 | %% 3 | %% } 4 | 5 | %% override panel -> { 6 |
7 | 8 |
9 | %% if ( $errors.size() ) { 10 | 18 | %% } 19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 | %% include 'simple/_/form/input.tx' { type => 'password', name => 'password', 27 | %% title => 'Password', 28 | %% help => 'You will need your password to login.', 29 | %% value => $form_password, 30 | %% }; 31 | 32 | %% include 'simple/_/form/input.tx' { type => 'password', name => 'confirm', 33 | %% title => 'Confirm password', 34 | %% help => mark_raw('Just to annoy you be sure it is correct.'), 35 | %% value => $form_confirm, 36 | %% }; 37 | 38 | 39 | 40 |
41 |
42 | %% } 43 | -------------------------------------------------------------------------------- /Web/templates/simple/user/index.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade simple::_::layout { title => 'People ' ~ $profile.username, 2 | %% 3 | %% } 4 | 5 | %% override panel -> { 6 | 7 |
8 |
9 | 10 |
11 |
12 |

[% $profile.username %]

13 |
14 |
15 | %% if ( $person.is_following_person( $profile.id )) { 16 |
17 | 18 | 19 |
20 | %% } else { 21 |
22 | 23 | 24 |
25 | %% } 26 | 27 |
28 |
29 | 30 |
31 |
32 |
33 | 34 |
35 | [% $profile_about %] 36 |
37 | 38 |
39 |
40 |
41 | 42 | 43 | 44 |
45 | %% for $profile.get_publically_followed_blogs -> $blog { 46 | %% include 'simple/_/_blog_sidecard.tx' { blog => $blog }; 47 |
48 | %% } 49 |
50 | 51 | 52 | 53 | %% } -------------------------------------------------------------------------------- /Web/templates/simple/user/settings.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade simple::_::layout { title => 'Your Settings', 2 | %% 3 | %% } 4 | 5 | %% override panel -> { 6 | 7 | %% include 'simple/user/settings/_navtabs.tx' { tab => 'settings' } 8 | 9 | 10 |
11 | 12 |
13 | %% if ( $errors.size() ) { 14 | 22 | %% } 23 |
24 |
25 | 26 | 27 |
28 |
29 |

Settings

30 |
31 |
32 | 33 |
34 | 35 | 36 |
37 | 38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 |
48 |
49 | 50 | %% } 51 | -------------------------------------------------------------------------------- /Web/templates/simple/user/settings/_navtabs.tx: -------------------------------------------------------------------------------- 1 | 2 |
3 | 9 |
10 | -------------------------------------------------------------------------------- /Web/templates/simple/user/settings/email.html.tx: -------------------------------------------------------------------------------- 1 | 2 | %% cascade simple::_::layout { title => 'Your Settings - Email', 3 | %% 4 | %% } 5 | 6 | %% override panel -> { 7 | 8 | %% include 'simple/user/settings/_navtabs.tx' { tab => 'email' } 9 | 10 |
11 | 12 |
13 | %% if ( $errors.size() ) { 14 | 22 | %% } 23 |
24 |
25 | 26 | 27 |
28 |
29 |

Change email address

30 |
31 |
32 | %% include 'simple/_/form/input.tx' { type => 'email', name => 'email', 33 | %% title => 'Email address', 34 | %% help => 'New email address', 35 | %% value => $form_email, 36 | %% placeholder => $c.stash.person.email, 37 | %% }; 38 | 39 | %% include 'simple/_/form/input.tx' { type => 'password', name => 'password', 40 | %% title => 'Password', 41 | %% help => 'To ensure it is you changing your email address.', 42 | %% value => $form_password, 43 | %% }; 44 | 45 |
46 |
47 |
48 |
49 | %% } -------------------------------------------------------------------------------- /Web/templates/simple/user/settings/following.html.tx: -------------------------------------------------------------------------------- 1 | 2 | %% cascade simple::_::layout { title => 'Your Settings - Email', 3 | %% 4 | %% } 5 | 6 | %% override panel -> { 7 | 8 | %% include 'simple/user/settings/_navtabs.tx' { tab => 'following' } 9 | 10 |
11 | 12 |
13 | %% if ( $errors.size() ) { 14 | 22 | %% } 23 |
24 |
25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | %% for $c.stash('person').get_followed_blogs -> $blog { 39 | 40 | 41 | 42 | 54 | 55 | %% } 56 | 57 |
#BlogPublically Follow?
[% $~blog.index + 1 %][% $blog.title %] 43 |
44 | 45 |
46 | 47 | 48 | 49 | 50 |
51 | 52 |
53 |
58 | 59 |
60 | %% } -------------------------------------------------------------------------------- /Web/templates/simple/user/settings/password.html.tx: -------------------------------------------------------------------------------- 1 | %% cascade simple::_::layout { title => 'Your Settings - Password', 2 | %% 3 | %% } 4 | 5 | 6 | 7 | %% override panel -> { 8 | 9 | %% include 'simple/user/settings/_navtabs.tx' { tab => 'password' } 10 | 11 | 12 |
13 | 14 |
15 | %% if ( $errors.size() ) { 16 | 24 | %% } 25 |
26 |
27 | 28 | 29 |
30 |
31 |

Change Password

32 |

You will be logged out and then must log in with your new password.

33 |
34 |
35 | %% include 'simple/_/form/input.tx' { type => 'password', name => 'password', 36 | %% title => 'Current password.', 37 | %% help => 'Your current password used to login.', 38 | %% value => $form_password, 39 | %% }; 40 | 41 | %% include 'simple/_/form/input.tx' { type => 'password', name => 'new_password', 42 | %% title => 'New Password', 43 | %% help => 'Your new password', 44 | %% value => $form_new_password, 45 | %% }; 46 | 47 | %% include 'simple/_/form/input.tx' { type => 'password', name => 'confirm', 48 | %% title => 'Confirm password', 49 | %% help => 'Your new password, again, to be sure we understand one another.', 50 | %% value => $form_confirm, 51 | %% }; 52 | 53 |
54 |
55 |
56 |
57 | 58 | %% } -------------------------------------------------------------------------------- /system/setup-debian.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #== 3 | # Script to set up a new Debian 11 machine to support BlogDB with docker & dex. 4 | #== 5 | 6 | #if $UID -ne 0; then 7 | # echo "Error: This script must be run as root."; 8 | # exit -1; 9 | #fi 10 | 11 | #== 12 | # Update the packages and install supporting software. 13 | #== 14 | apt-get update -y; 15 | apt-get upgrade -y; 16 | apt-get install -y git build-essential cpanminus liblocal-lib-perl expect sudo; 17 | 18 | #== 19 | # Install postgresql, some database helpers, and set our DBs up. 20 | #== 21 | apt-get install -y postgresql postgresql-contrib postgresql-client 22 | 23 | # Make a script to make createdb run with env vars. 24 | cat > /bin/createdb_from_env <<'EOF'; 25 | #!/usr/bin/env expect 26 | 27 | set username $env(DB_USER) 28 | set password $env(DB_PASS) 29 | set database $env(DB_NAME) 30 | 31 | spawn createdb -h localhost -U $username -W $database 32 | expect "Password:" 33 | send "$password\r\n" 34 | expect eof 35 | EOF 36 | 37 | # Make a script to make createuser run with env vars. 38 | cat > /bin/createuser_from_env <<'EOF'; 39 | #!/usr/bin/env expect 40 | 41 | set username $env(DB_USER) 42 | set password $env(DB_PASS) 43 | 44 | spawn createuser -d $username -P 45 | expect "Enter password for new role:" 46 | send "$password\r\n" 47 | expect "Enter it again:" 48 | send "$password\r\n" 49 | expect eof 50 | EOF 51 | 52 | # Make a script to import a DB file. 53 | cat > /bin/psql_import_from_env <<'EOF'; 54 | #!/usr/bin/env expect 55 | 56 | set username $env(DB_USER) 57 | set password $env(DB_PASS) 58 | set database $env(DB_NAME) 59 | set filename $env(DB_FILE) 60 | 61 | spawn psql -U $username -W --host localhost -f $filename $database 62 | expect "Password:" 63 | send "$password\r\n" 64 | expect eof 65 | EOF 66 | 67 | # Make a script to make createuser run with env vars. 68 | cat > /bin/randpass <<'EOF'; 69 | #!/usr/bin/env perl 70 | 71 | my @chars = ( 'A' .. 'Z', 'a' .. 'z', 0 .. 9 ); 72 | 73 | print map { $chars[int rand @chars] } ( 0 .. 48 ); 74 | EOF 75 | 76 | # Make all our scripts executable. 77 | chmod 755 /bin/createdb_from_env 78 | chmod 755 /bin/createuser_from_env 79 | chmod 755 /bin/psql_import_from_env 80 | chmod 755 /bin/randpass 81 | 82 | # Create a random password for the db, store it in a file so we can refer to it later. 83 | randpass > /root/.database_password 84 | 85 | DB_USER="blogdb" DB_PASS=$(cat /root/.database_password) sudo -Eu postgres createuser_from_env 86 | DB_USER="blogdb" DB_PASS=$(cat /root/.database_password) DB_NAME="blogdb" createdb_from_env 87 | DB_USER="blogdb" DB_PASS=$(cat /root/.database_password) DB_NAME="minion" createdb_from_env 88 | 89 | #== 90 | # Install Docker 91 | #== 92 | 93 | # Install packages we need to have in order to install docker. 94 | apt-get update -y; 95 | apt-get install -y ca-certificates curl gnupg lsb-release; 96 | 97 | # Get keys from docker for their packages. 98 | curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg; 99 | 100 | # Add the docker apt repo. 101 | echo \ 102 | "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian \ 103 | $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list; 104 | 105 | # Finally, actually install docker. 106 | apt-get update -y; 107 | apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose; 108 | 109 | # Setup local::lib for the future and then enable it for the rest of this script. 110 | 111 | echo 'eval "$(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib)"' >> /root/.bashrc; 112 | source /root/.bashrc; 113 | 114 | # Install App::Dex 115 | cpanm App::Dex 116 | 117 | echo 118 | echo 119 | echo "The system is now setup to run docker containers from root." 120 | echo 121 | echo "You should exit this shell and reconnect, or source ~/.bashrc" 122 | echo "so that your perl environment will be setup and dex will run." 123 | echo 124 | echo "Then, get BlogDB and proceed:" 125 | echo "SSH: git@github.com:symkat/BlogDB.git" 126 | echo "HTTP: https://github.com/symkat/BlogDB.git" 127 | echo 128 | -------------------------------------------------------------------------------- /system/systemd/blogdb.screenshot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=BlogDB Screenshot Service 3 | After=docker.service 4 | Requires=docker.service 5 | 6 | [Service] 7 | ExecStartPre=-/usr/bin/docker stop blogdb-screenshot 8 | ExecStartPre=-/usr/bin/docker rm blogdb-screenshot 9 | ExecStartPre=/usr/bin/docker pull elestio/ws-screenshot.slim 10 | ExecStart=/usr/bin/docker run --name blogdb-screenshot -p 127.0.0.1:5000:3000 elestio/ws-screenshot.slim 11 | Restart=on-failure 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /system/systemd/blogdb.web.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=BlogDB Web Service 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart=/home/vagrant/perl5/bin/morbo ./script/blogdb_web daemon 7 | Restart=on-failure 8 | User=vagrant 9 | Group=vagrant 10 | WorkingDirectory=/home/vagrant/BlogDB/Web 11 | Environment="PERL_MB_OPT=--install_base \"/home/vagrant/perl5\"" 12 | Environment="PERL_MM_OPT=INSTALL_BASE=/home/vagrant/perl5" 13 | Environment="PERL5LIB=/home/vagrant/perl5/lib/perl5" 14 | Environment="PATH=/home/vagrant/perl5/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games" 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /system/systemd/blogdb.worker.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=BlogDB Web Service 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart=/home/vagrant/BlogDB/Web/script/blogdb_web minion worker 7 | Restart=on-failure 8 | User=vagrant 9 | Group=vagrant 10 | WorkingDirectory=/home/vagrant/BlogDB/Web 11 | Environment="PERL_MB_OPT=--install_base \"/home/vagrant/perl5\"" 12 | Environment="PERL_MM_OPT=INSTALL_BASE=/home/vagrant/perl5" 13 | Environment="PERL5LIB=/home/vagrant/perl5/lib/perl5" 14 | Environment="PATH=/home/vagrant/perl5/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games" 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /system/vagrant-post-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Install Supporting Software 4 | sudo apt-get install -y vim unzip libssl-dev libz-dev libpq-dev libexpat1-dev 5 | 6 | # Allow vagrant user to use docker. 7 | sudo usermod -a -G docker vagrant 8 | 9 | # Setup local::lib 10 | eval $(perl -Mlocal::lib) 11 | echo 'eval $(perl -Mlocal::lib)' >> /home/vagrant/.bashrc 12 | 13 | # Install packages 14 | cpanm Dist::Zilla App::Dex; cpanm Dist::Zilla App::Dex 15 | 16 | # Build DB package 17 | cd /home/vagrant/BlogDB/DB 18 | dzil build 19 | cpanm BlogDB-DB-*.tar.gz; cpanm BlogDB-DB-*.tar.gz 20 | 21 | # Install web package dependancies. 22 | cd /home/vagrant/BlogDB/Web 23 | cpanm --installdeps .; cpanm --installdeps . 24 | 25 | # Install systemd files. 26 | sudo cp /home/vagrant/BlogDB/system/systemd/blogdb.screenshot.service /etc/systemd/system 27 | sudo cp /home/vagrant/BlogDB/system/systemd/blogdb.web.service /etc/systemd/system 28 | sudo cp /home/vagrant/BlogDB/system/systemd/blogdb.worker.service /etc/systemd/system 29 | 30 | # Import the database. 31 | DB_FILE="/home/vagrant/BlogDB/DB/etc/schema.sql" \ 32 | DB_PASS=$(sudo cat /root/.database_password) \ 33 | DB_USER="blogdb" DB_NAME="blogdb" psql_import_from_env 34 | 35 | # Install configuration file. 36 | cat > /home/vagrant/BlogDB/Web/blogdb.yml <