From dcde57aa5fd037f30603b9a691664ffc62217d89 Mon Sep 17 00:00:00 2001 From: Kaitlyn Parkhurst Date: Tue, 6 Sep 2022 19:14:19 -0700 Subject: [PATCH] Initial DB. --- DB/bin/create-classes | 18 ++ DB/dist.ini | 21 +++ DB/etc/schema.sql | 40 +++++ DB/lib/MJB/DB.pm | 22 +++ DB/lib/MJB/DB/Result/AuthPassword.pm | 176 +++++++++++++++++++ DB/lib/MJB/DB/Result/AuthToken.pm | 128 ++++++++++++++ DB/lib/MJB/DB/Result/Person.pm | 233 ++++++++++++++++++++++++++ DB/lib/MJB/DB/Result/PersonSetting.pm | 151 +++++++++++++++++ 8 files changed, 789 insertions(+) create mode 100755 DB/bin/create-classes create mode 100644 DB/dist.ini create mode 100644 DB/etc/schema.sql create mode 100644 DB/lib/MJB/DB.pm create mode 100644 DB/lib/MJB/DB/Result/AuthPassword.pm create mode 100644 DB/lib/MJB/DB/Result/AuthToken.pm create mode 100644 DB/lib/MJB/DB/Result/Person.pm create mode 100644 DB/lib/MJB/DB/Result/PersonSetting.pm diff --git a/DB/bin/create-classes b/DB/bin/create-classes new file mode 100755 index 0000000..d81c7d3 --- /dev/null +++ b/DB/bin/create-classes @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +CLASS_NAME="MJB::DB" + +# Generate a random 8 character name for the docker container that holds the PSQL +# database. +PSQL_NAME=$(cat /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z' | fold -w 8 | head -n 1) + +# Launch a PSQL Instance +PSQL_DOCKER=`docker run --rm --name $PSQL_NAME -e POSTGRES_PASSWORD=dbic -e POSTGRES_USER=dbic -e POSTGRES_DB=dbic -d \ + --mount type=bind,src=$PWD/etc/schema.sql,dst=/docker-entrypoint-initdb.d/schema.sql postgres:11` + +docker run --rm --link $PSQL_NAME:psqldb --mount type=bind,src=$PWD,dst=/app symkat/schema_builder /bin/build-schema $CLASS_NAME + +docker kill $PSQL_DOCKER + +sudo chown -R $USER:$USER lib + diff --git a/DB/dist.ini b/DB/dist.ini new file mode 100644 index 0000000..98cdd81 --- /dev/null +++ b/DB/dist.ini @@ -0,0 +1,21 @@ +name = MJB-DB +author = Kaitlyn Parkhurst +license = Perl_5 +copyright_holder = Kaitlyn Parkhurst +copyright_year = 2022 +abstract = MyJekyllBlog's Database +version = 1 + +[@Basic] + +[Prereqs] +DBIx::Class::InflateColumn::Serializer = 0 +DBIx::Class::Schema::Config = 0 +DBIx::Class::DeploymentHandler = 0 +DBIx::Class::Schema::ResultSetNames = 0 +MooseX::AttributeShortcuts = 0 +MooseX::Getopt = 0 +Data::GUID = 0 +DBD::Pg = 0 +Crypt::Eksblowfish::Bcrypt = 0 +Crypt::Random = 0 diff --git a/DB/etc/schema.sql b/DB/etc/schema.sql new file mode 100644 index 0000000..9dc45cc --- /dev/null +++ b/DB/etc/schema.sql @@ -0,0 +1,40 @@ +CREATE EXTENSION IF NOT EXISTS citext; + +CREATE TABLE person ( + id serial PRIMARY KEY, + name text not null, + email citext not null unique, + is_enabled boolean not null default true, + is_admin boolean not null default false, + created_at timestamptz not null default current_timestamp +); + +-- Settings for a given user. | Use with care, add things to the data model when you should. +CREATE TABLE person_settings ( + id serial PRIMARY KEY, + person_id int not null references person(id), + name text not null, + value json not null default '{}', + created_at timestamptz not null default current_timestamp, + + -- Allow ->find_or_new_related() + CONSTRAINT unq_person_id_name UNIQUE(person_id, name) +); + +CREATE TABLE auth_password ( + person_id int not null unique references person(id), + password text not null, + salt text not null, + updated_at timestamptz not null default current_timestamp, + created_at timestamptz not null default current_timestamp +); + +CREATE TABLE auth_token ( + id serial PRIMARY KEY, + person_id int not null references person(id), + scope text not null, + token text not null, + created_at timestamptz not null default current_timestamp +); + + diff --git a/DB/lib/MJB/DB.pm b/DB/lib/MJB/DB.pm new file mode 100644 index 0000000..5a7a70e --- /dev/null +++ b/DB/lib/MJB/DB.pm @@ -0,0 +1,22 @@ +use utf8; +package MJB::DB; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +use strict; +use warnings; + +use base 'DBIx::Class::Schema'; + +__PACKAGE__->load_components("Schema::Config", "Schema::ResultSetNames"); + +__PACKAGE__->load_namespaces; + + +# Created by DBIx::Class::Schema::Loader v0.07049 @ 2022-09-07 02:12:13 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:VjO6UzjruK0OYvT2H/KKtg + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/DB/lib/MJB/DB/Result/AuthPassword.pm b/DB/lib/MJB/DB/Result/AuthPassword.pm new file mode 100644 index 0000000..4d62c21 --- /dev/null +++ b/DB/lib/MJB/DB/Result/AuthPassword.pm @@ -0,0 +1,176 @@ +use utf8; +package MJB::DB::Result::AuthPassword; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +MJB::DB::Result::AuthPassword + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 COMPONENTS LOADED + +=over 4 + +=item * L + +=item * L + +=back + +=cut + +__PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("auth_password"); + +=head1 ACCESSORS + +=head2 person_id + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +=head2 password + + data_type: 'text' + is_nullable: 0 + +=head2 salt + + data_type: 'text' + is_nullable: 0 + +=head2 updated_at + + data_type: 'timestamp with time zone' + default_value: current_timestamp + is_nullable: 0 + +=head2 created_at + + data_type: 'timestamp with time zone' + default_value: current_timestamp + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "person_id", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "password", + { data_type => "text", is_nullable => 0 }, + "salt", + { data_type => "text", is_nullable => 0 }, + "updated_at", + { + data_type => "timestamp with time zone", + default_value => \"current_timestamp", + is_nullable => 0, + }, + "created_at", + { + data_type => "timestamp with time zone", + default_value => \"current_timestamp", + is_nullable => 0, + }, +); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("auth_password_person_id_key", ["person_id"]); + +=head1 RELATIONS + +=head2 person + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "person", + "MJB::DB::Result::Person", + { id => "person_id" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07049 @ 2022-09-07 02:12:13 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:y9fTRaD/zpn3Z+KL3HZv1Q + +__PACKAGE__->set_primary_key('person_id'); + +use Crypt::Eksblowfish::Bcrypt qw( bcrypt_hash en_base64 de_base64 ); +use Crypt::Random; + +sub check_password { + my ( $self, $password ) = @_; + return de_base64($self->password) eq bcrypt_hash({ + key_nul => 1, + cost => 8, + salt => de_base64($self->salt), + }, $password ); +} + +sub set_password { + my ( $self, $password ) = @_; + $self->_fill_password( $password ); + $self->insert; + return $self; +} + +sub update_password { + my ( $self, $password ) = @_; + $self->_fill_password( $password ); + $self->update; + return $self; +} + +sub _fill_password { + my ( $self, $password ) = @_; + + my $salt = random_salt(); + + $self->password( + en_base64( + bcrypt_hash({ + key_nul => 1, + cost => 8, + salt => $salt, + }, $password ) + ) + ); + + $self->salt( en_base64($salt) ); +} + +sub random_salt { + Crypt::Random::makerandom_octet( Length => 16 ); +} + +1; diff --git a/DB/lib/MJB/DB/Result/AuthToken.pm b/DB/lib/MJB/DB/Result/AuthToken.pm new file mode 100644 index 0000000..063cdc9 --- /dev/null +++ b/DB/lib/MJB/DB/Result/AuthToken.pm @@ -0,0 +1,128 @@ +use utf8; +package MJB::DB::Result::AuthToken; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +MJB::DB::Result::AuthToken + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 COMPONENTS LOADED + +=over 4 + +=item * L + +=item * L + +=back + +=cut + +__PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("auth_token"); + +=head1 ACCESSORS + +=head2 id + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + sequence: 'auth_token_id_seq' + +=head2 person_id + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +=head2 scope + + data_type: 'text' + is_nullable: 0 + +=head2 token + + data_type: 'text' + is_nullable: 0 + +=head2 created_at + + data_type: 'timestamp with time zone' + default_value: current_timestamp + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "id", + { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + sequence => "auth_token_id_seq", + }, + "person_id", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "scope", + { data_type => "text", is_nullable => 0 }, + "token", + { data_type => "text", is_nullable => 0 }, + "created_at", + { + data_type => "timestamp with time zone", + default_value => \"current_timestamp", + is_nullable => 0, + }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("id"); + +=head1 RELATIONS + +=head2 person + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "person", + "MJB::DB::Result::Person", + { id => "person_id" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07049 @ 2022-09-07 02:12:13 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:QswwNuQ8rXDUWbDi3kaEYA + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/DB/lib/MJB/DB/Result/Person.pm b/DB/lib/MJB/DB/Result/Person.pm new file mode 100644 index 0000000..fc266c6 --- /dev/null +++ b/DB/lib/MJB/DB/Result/Person.pm @@ -0,0 +1,233 @@ +use utf8; +package MJB::DB::Result::Person; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +MJB::DB::Result::Person + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 COMPONENTS LOADED + +=over 4 + +=item * L + +=item * L + +=back + +=cut + +__PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("person"); + +=head1 ACCESSORS + +=head2 id + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + sequence: 'person_id_seq' + +=head2 name + + data_type: 'text' + is_nullable: 0 + +=head2 email + + data_type: 'citext' + is_nullable: 0 + +=head2 is_enabled + + data_type: 'boolean' + default_value: true + is_nullable: 0 + +=head2 is_admin + + data_type: 'boolean' + default_value: false + is_nullable: 0 + +=head2 created_at + + data_type: 'timestamp with time zone' + default_value: current_timestamp + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "id", + { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + sequence => "person_id_seq", + }, + "name", + { data_type => "text", is_nullable => 0 }, + "email", + { data_type => "citext", is_nullable => 0 }, + "is_enabled", + { data_type => "boolean", default_value => \"true", is_nullable => 0 }, + "is_admin", + { data_type => "boolean", default_value => \"false", is_nullable => 0 }, + "created_at", + { + data_type => "timestamp with time zone", + default_value => \"current_timestamp", + is_nullable => 0, + }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("id"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("person_email_key", ["email"]); + +=head1 RELATIONS + +=head2 auth_password + +Type: might_have + +Related object: L + +=cut + +__PACKAGE__->might_have( + "auth_password", + "MJB::DB::Result::AuthPassword", + { "foreign.person_id" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 auth_tokens + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "auth_tokens", + "MJB::DB::Result::AuthToken", + { "foreign.person_id" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 person_settings + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "person_settings", + "MJB::DB::Result::PersonSetting", + { "foreign.person_id" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07049 @ 2022-09-07 02:12:13 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:t8nEGpGF7PyRX9J3V7R9Rw + +use Data::GUID; + +sub setting { + my ( $self, $setting, $value ) = @_; + + if ( defined $value ) { + my $rs = $self->find_or_new_related( 'person_settings', { name => $setting } ); + $rs->value( ref $value ? $value : { value => $value } ); + + $rs->update if $rs->in_storage; + $rs->insert unless $rs->in_storage; + + return $value; + } else { + my $result = $self->find_related('person_settings', { name => $setting }); + return undef unless $result; + return $self->_get_setting_value($result); + } +} + +sub _get_setting_value { + my ( $self, $setting ) = @_; + + if ( ref $setting->value eq 'HASH' and keys %{$setting->value} == 1 and exists $setting->value->{value} ) { + return $setting->value->{value}; + } + + return $setting->value; +} + +sub get_settings { + my ( $self ) = @_; + + my $return = {}; + + foreach my $setting ( $self->search_related( 'person_settings', {} )->all ) { + $return->{${\($setting->name)}} = $self->_get_setting_value($setting); + } + + return $return; +} + +sub create_auth_token { + my ( $self, $scope ) = @_; + + my $token = Data::GUID->guid_string; + + $self->create_related( 'auth_tokens', { + token => $token, + scope => $scope, + }); + + return $token; +} + +1; diff --git a/DB/lib/MJB/DB/Result/PersonSetting.pm b/DB/lib/MJB/DB/Result/PersonSetting.pm new file mode 100644 index 0000000..74eac5a --- /dev/null +++ b/DB/lib/MJB/DB/Result/PersonSetting.pm @@ -0,0 +1,151 @@ +use utf8; +package MJB::DB::Result::PersonSetting; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +MJB::DB::Result::PersonSetting + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 COMPONENTS LOADED + +=over 4 + +=item * L + +=item * L + +=back + +=cut + +__PACKAGE__->load_components("InflateColumn::DateTime", "InflateColumn::Serializer"); + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("person_settings"); + +=head1 ACCESSORS + +=head2 id + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + sequence: 'person_settings_id_seq' + +=head2 person_id + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +=head2 name + + data_type: 'text' + is_nullable: 0 + +=head2 value + + data_type: 'json' + default_value: '{}' + is_nullable: 0 + serializer_class: 'JSON' + +=head2 created_at + + data_type: 'timestamp with time zone' + default_value: current_timestamp + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "id", + { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + sequence => "person_settings_id_seq", + }, + "person_id", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "name", + { data_type => "text", is_nullable => 0 }, + "value", + { + data_type => "json", + default_value => "{}", + is_nullable => 0, + serializer_class => "JSON", + }, + "created_at", + { + data_type => "timestamp with time zone", + default_value => \"current_timestamp", + is_nullable => 0, + }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("id"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("unq_person_id_name", ["person_id", "name"]); + +=head1 RELATIONS + +=head2 person + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "person", + "MJB::DB::Result::Person", + { id => "person_id" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07049 @ 2022-09-07 02:12:13 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:up8WXyHIVQXePFBYFKs5xA + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1;