2019-03-22 15:56:49 +00:00
|
|
|
|
package Travelynx::Controller::Account;
|
2021-04-20 19:59:17 +00:00
|
|
|
|
|
2023-02-19 08:35:38 +00:00
|
|
|
|
# Copyright (C) 2020-2023 Daniel Friesel
|
2020-11-27 21:12:56 +00:00
|
|
|
|
#
|
2021-01-29 17:32:13 +00:00
|
|
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
2019-03-22 15:56:49 +00:00
|
|
|
|
use Mojo::Base 'Mojolicious::Controller';
|
|
|
|
|
|
|
|
|
|
use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64);
|
2023-01-08 08:40:49 +00:00
|
|
|
|
use JSON;
|
2023-06-04 12:28:04 +00:00
|
|
|
|
use Mojo::Util qw(xml_escape);
|
|
|
|
|
use Text::Markdown;
|
2023-01-08 08:40:49 +00:00
|
|
|
|
use UUID::Tiny qw(:std);
|
2019-03-22 15:56:49 +00:00
|
|
|
|
|
2023-02-27 21:14:54 +00:00
|
|
|
|
my %visibility_itoa = (
|
|
|
|
|
100 => 'public',
|
|
|
|
|
80 => 'travelynx',
|
|
|
|
|
60 => 'followers',
|
|
|
|
|
30 => 'unlisted',
|
|
|
|
|
10 => 'private',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
my %visibility_atoi = (
|
|
|
|
|
public => 100,
|
|
|
|
|
travelynx => 80,
|
|
|
|
|
followers => 60,
|
|
|
|
|
unlisted => 30,
|
|
|
|
|
private => 10,
|
|
|
|
|
);
|
|
|
|
|
|
2022-07-30 08:41:31 +00:00
|
|
|
|
# Internal Helpers
|
|
|
|
|
|
2019-03-22 15:56:49 +00:00
|
|
|
|
sub hash_password {
|
|
|
|
|
my ($password) = @_;
|
|
|
|
|
my @salt_bytes = map { int( rand(255) ) + 1 } ( 1 .. 16 );
|
2020-07-27 16:53:22 +00:00
|
|
|
|
my $salt = en_base64( pack( 'C[16]', @salt_bytes ) );
|
2019-03-22 15:56:49 +00:00
|
|
|
|
|
2022-03-07 17:12:43 +00:00
|
|
|
|
return bcrypt( substr( $password, 0, 10000 ), '$2a$12$' . $salt );
|
2019-03-22 15:56:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub make_token {
|
|
|
|
|
return create_uuid_as_string(UUID_V4);
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-30 08:41:31 +00:00
|
|
|
|
sub send_registration_mail {
|
|
|
|
|
my ( $self, %opt ) = @_;
|
|
|
|
|
|
|
|
|
|
my $email = $opt{email};
|
|
|
|
|
my $token = $opt{token};
|
|
|
|
|
my $user = $opt{user};
|
|
|
|
|
my $user_id = $opt{user_id};
|
|
|
|
|
my $ip = $opt{ip};
|
|
|
|
|
my $date = DateTime->now( time_zone => 'Europe/Berlin' )
|
|
|
|
|
->strftime('%d.%m.%Y %H:%M:%S %z');
|
|
|
|
|
|
|
|
|
|
my $ua = $self->req->headers->user_agent;
|
|
|
|
|
my $reg_url = $self->url_for('reg')->to_abs->scheme('https');
|
|
|
|
|
my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
|
|
|
|
|
|
|
|
|
|
my $body = "Hallo, ${user}!\n\n";
|
|
|
|
|
$body .= "Mit deiner E-Mail-Adresse (${email}) wurde ein Account bei\n";
|
|
|
|
|
$body .= "travelynx angelegt.\n\n";
|
|
|
|
|
$body
|
|
|
|
|
.= "Falls die Registrierung von dir ausging, kannst du den Account unter\n";
|
|
|
|
|
$body .= "${reg_url}/${user_id}/${token}\n";
|
|
|
|
|
$body .= "freischalten.\n\n";
|
|
|
|
|
$body
|
|
|
|
|
.= "Falls nicht, ignoriere diese Mail bitte. Nach etwa 48 Stunden wird deine\n";
|
|
|
|
|
$body
|
|
|
|
|
.= "Mail-Adresse erneut zur Registrierung freigeschaltet. Falls auch diese fehlschlägt,\n";
|
|
|
|
|
$body
|
|
|
|
|
.= "werden wir sie dauerhaft sperren und keine Mails mehr dorthin schicken.\n\n";
|
|
|
|
|
$body .= "Daten zur Registrierung:\n";
|
|
|
|
|
$body .= " * Datum: ${date}\n";
|
|
|
|
|
$body .= " * Client: ${ip}\n";
|
|
|
|
|
$body .= " * UserAgent: ${ua}\n\n\n";
|
|
|
|
|
$body .= "Impressum: ${imprint_url}\n";
|
|
|
|
|
|
|
|
|
|
return $self->sendmail->custom( $email, 'Registrierung bei travelynx',
|
|
|
|
|
$body );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub send_address_confirmation_mail {
|
|
|
|
|
my ( $self, $email, $token ) = @_;
|
|
|
|
|
|
|
|
|
|
my $name = $self->current_user->{name};
|
|
|
|
|
my $ip = $self->req->headers->header('X-Forwarded-For');
|
|
|
|
|
my $ua = $self->req->headers->user_agent;
|
|
|
|
|
my $date = DateTime->now( time_zone => 'Europe/Berlin' )
|
|
|
|
|
->strftime('%d.%m.%Y %H:%M:%S %z');
|
|
|
|
|
|
|
|
|
|
# In case Mojolicious is not running behind a reverse proxy
|
|
|
|
|
$ip
|
|
|
|
|
//= sprintf( '%s:%s', $self->tx->remote_address, $self->tx->remote_port );
|
|
|
|
|
my $confirm_url = $self->url_for('confirm_mail')->to_abs->scheme('https');
|
|
|
|
|
my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
|
|
|
|
|
|
|
|
|
|
my $body = "Hallo ${name},\n\n";
|
|
|
|
|
$body .= "Bitte bestätige unter <${confirm_url}/${token}>,\n";
|
|
|
|
|
$body .= "dass du mit dieser Adresse E-Mail empfangen kannst.\n\n";
|
|
|
|
|
$body
|
|
|
|
|
.= "Du erhältst diese Mail, da eine Änderung der deinem travelynx-Account\n";
|
|
|
|
|
$body .= "zugeordneten Mail-Adresse beantragt wurde.\n\n";
|
|
|
|
|
$body .= "Daten zur Anfrage:\n";
|
|
|
|
|
$body .= " * Datum: ${date}\n";
|
|
|
|
|
$body .= " * Client: ${ip}\n";
|
|
|
|
|
$body .= " * UserAgent: ${ua}\n\n\n";
|
|
|
|
|
$body .= "Impressum: ${imprint_url}\n";
|
|
|
|
|
|
|
|
|
|
return $self->sendmail->custom( $email,
|
|
|
|
|
'travelynx: Mail-Adresse bestätigen', $body );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub send_name_notification_mail {
|
|
|
|
|
my ( $self, $old_name, $new_name ) = @_;
|
|
|
|
|
|
|
|
|
|
my $ip = $self->req->headers->header('X-Forwarded-For');
|
|
|
|
|
my $ua = $self->req->headers->user_agent;
|
|
|
|
|
my $date = DateTime->now( time_zone => 'Europe/Berlin' )
|
|
|
|
|
->strftime('%d.%m.%Y %H:%M:%S %z');
|
|
|
|
|
|
|
|
|
|
# In case Mojolicious is not running behind a reverse proxy
|
|
|
|
|
$ip
|
|
|
|
|
//= sprintf( '%s:%s', $self->tx->remote_address, $self->tx->remote_port );
|
|
|
|
|
my $confirm_url = $self->url_for('confirm_mail')->to_abs->scheme('https');
|
|
|
|
|
my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
|
|
|
|
|
|
|
|
|
|
my $body = "Hallo ${new_name},\n\n";
|
|
|
|
|
$body .= "Der Name deines Travelynx-Accounts wurde erfolgreich geändert.\n";
|
|
|
|
|
$body
|
|
|
|
|
.= "Bitte beachte, dass du dich ab sofort nur mit dem neuen Namen anmelden kannst.\n\n";
|
|
|
|
|
$body .= "Alter Name: ${old_name}\n\n";
|
|
|
|
|
$body .= "Neue Name: ${new_name}\n\n";
|
|
|
|
|
$body .= "Daten zur Anfrage:\n";
|
|
|
|
|
$body .= " * Datum: ${date}\n";
|
|
|
|
|
$body .= " * Client: ${ip}\n";
|
|
|
|
|
$body .= " * UserAgent: ${ua}\n\n\n";
|
|
|
|
|
$body .= "Impressum: ${imprint_url}\n";
|
|
|
|
|
|
|
|
|
|
return $self->sendmail->custom( $self->current_user->{email},
|
|
|
|
|
'travelynx: Name geändert', $body );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub send_password_notification_mail {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
my $user = $self->current_user->{name};
|
|
|
|
|
my $email = $self->current_user->{email};
|
|
|
|
|
my $ip = $self->req->headers->header('X-Forwarded-For');
|
|
|
|
|
my $ua = $self->req->headers->user_agent;
|
|
|
|
|
my $date = DateTime->now( time_zone => 'Europe/Berlin' )
|
|
|
|
|
->strftime('%d.%m.%Y %H:%M:%S %z');
|
|
|
|
|
|
|
|
|
|
# In case Mojolicious is not running behind a reverse proxy
|
|
|
|
|
$ip
|
|
|
|
|
//= sprintf( '%s:%s', $self->tx->remote_address, $self->tx->remote_port );
|
|
|
|
|
my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
|
|
|
|
|
|
|
|
|
|
my $body = "Hallo ${user},\n\n";
|
|
|
|
|
$body
|
|
|
|
|
.= "Das Passwort deines travelynx-Accounts wurde soeben geändert.\n\n";
|
|
|
|
|
$body .= "Daten zur Änderung:\n";
|
|
|
|
|
$body .= " * Datum: ${date}\n";
|
|
|
|
|
$body .= " * Client: ${ip}\n";
|
|
|
|
|
$body .= " * UserAgent: ${ua}\n\n\n";
|
|
|
|
|
$body .= "Impressum: ${imprint_url}\n";
|
|
|
|
|
|
|
|
|
|
$self->sendmail->custom( $email, 'travelynx: Passwort geändert', $body );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub send_lostpassword_confirmation_mail {
|
|
|
|
|
my ( $self, %opt ) = @_;
|
|
|
|
|
my $email = $opt{email};
|
|
|
|
|
my $name = $opt{name};
|
|
|
|
|
my $uid = $opt{uid};
|
|
|
|
|
my $token = $opt{token};
|
|
|
|
|
|
|
|
|
|
my $ip = $self->req->headers->header('X-Forwarded-For');
|
|
|
|
|
my $ua = $self->req->headers->user_agent;
|
|
|
|
|
my $date = DateTime->now( time_zone => 'Europe/Berlin' )
|
|
|
|
|
->strftime('%d.%m.%Y %H:%M:%S %z');
|
|
|
|
|
|
|
|
|
|
# In case Mojolicious is not running behind a reverse proxy
|
|
|
|
|
$ip
|
|
|
|
|
//= sprintf( '%s:%s', $self->tx->remote_address, $self->tx->remote_port );
|
|
|
|
|
my $recover_url = $self->url_for('recover')->to_abs->scheme('https');
|
|
|
|
|
my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
|
|
|
|
|
|
|
|
|
|
my $body = "Hallo ${name},\n\n";
|
|
|
|
|
$body .= "Unter ${recover_url}/${uid}/${token}\n";
|
|
|
|
|
$body
|
|
|
|
|
.= "kannst du ein neues Passwort für deinen travelynx-Account vergeben.\n\n";
|
|
|
|
|
$body
|
|
|
|
|
.= "Du erhältst diese Mail, da mit deinem Accountnamen und deiner Mail-Adresse\n";
|
|
|
|
|
$body
|
|
|
|
|
.= "ein Passwort-Reset angefordert wurde. Falls diese Anfrage nicht von dir\n";
|
|
|
|
|
$body .= "ausging, kannst du sie ignorieren.\n\n";
|
|
|
|
|
$body .= "Daten zur Anfrage:\n";
|
|
|
|
|
$body .= " * Datum: ${date}\n";
|
|
|
|
|
$body .= " * Client: ${ip}\n";
|
|
|
|
|
$body .= " * UserAgent: ${ua}\n\n\n";
|
|
|
|
|
$body .= "Impressum: ${imprint_url}\n";
|
|
|
|
|
|
|
|
|
|
my $success
|
|
|
|
|
= $self->sendmail->custom( $email, 'travelynx: Neues Passwort', $body );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub send_lostpassword_notification_mail {
|
|
|
|
|
my ( $self, $account ) = @_;
|
|
|
|
|
my $user = $account->{name};
|
|
|
|
|
my $email = $account->{email};
|
|
|
|
|
my $ip = $self->req->headers->header('X-Forwarded-For');
|
|
|
|
|
my $ua = $self->req->headers->user_agent;
|
|
|
|
|
my $date = DateTime->now( time_zone => 'Europe/Berlin' )
|
|
|
|
|
->strftime('%d.%m.%Y %H:%M:%S %z');
|
|
|
|
|
|
|
|
|
|
# In case Mojolicious is not running behind a reverse proxy
|
|
|
|
|
$ip
|
|
|
|
|
//= sprintf( '%s:%s', $self->tx->remote_address, $self->tx->remote_port );
|
|
|
|
|
my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
|
|
|
|
|
|
|
|
|
|
my $body = "Hallo ${user},\n\n";
|
|
|
|
|
$body .= "Das Passwort deines travelynx-Accounts wurde soeben über die";
|
|
|
|
|
$body .= " 'Passwort vergessen'-Funktion geändert.\n\n";
|
|
|
|
|
$body .= "Daten zur Änderung:\n";
|
|
|
|
|
$body .= " * Datum: ${date}\n";
|
|
|
|
|
$body .= " * Client: ${ip}\n";
|
|
|
|
|
$body .= " * UserAgent: ${ua}\n\n\n";
|
|
|
|
|
$body .= "Impressum: ${imprint_url}\n";
|
|
|
|
|
|
|
|
|
|
return $self->sendmail->custom( $email, 'travelynx: Passwort geändert',
|
|
|
|
|
$body );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Controllers
|
|
|
|
|
|
2019-03-22 15:56:49 +00:00
|
|
|
|
sub login_form {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
$self->render('login');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub do_login {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
my $user = $self->req->param('user');
|
|
|
|
|
my $password = $self->req->param('password');
|
|
|
|
|
|
|
|
|
|
# Keep cookies for 6 months
|
|
|
|
|
$self->session( expiration => 60 * 60 * 24 * 180 );
|
|
|
|
|
|
|
|
|
|
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
|
|
|
|
|
$self->render(
|
|
|
|
|
'login',
|
|
|
|
|
invalid => 'csrf',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
if ( $self->authenticate( $user, $password ) ) {
|
|
|
|
|
$self->redirect_to( $self->req->param('redirect_to') // '/' );
|
2020-07-27 16:53:22 +00:00
|
|
|
|
$self->users->mark_seen( uid => $self->current_user->{id} );
|
2019-03-22 15:56:49 +00:00
|
|
|
|
}
|
|
|
|
|
else {
|
2020-07-27 16:53:22 +00:00
|
|
|
|
my $data = $self->users->get_login_data( name => $user );
|
2019-03-22 15:56:49 +00:00
|
|
|
|
if ( $data and $data->{status} == 0 ) {
|
|
|
|
|
$self->render( 'login', invalid => 'confirmation' );
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$self->render( 'login', invalid => 'credentials' );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub registration_form {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
$self->render('register');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub register {
|
|
|
|
|
my ($self) = @_;
|
2021-04-20 19:59:17 +00:00
|
|
|
|
my $dt = $self->req->param('dt');
|
2019-03-22 15:56:49 +00:00
|
|
|
|
my $user = $self->req->param('user');
|
|
|
|
|
my $email = $self->req->param('email');
|
|
|
|
|
my $password = $self->req->param('password');
|
|
|
|
|
my $password2 = $self->req->param('password2');
|
|
|
|
|
my $ip = $self->req->headers->header('X-Forwarded-For');
|
|
|
|
|
|
|
|
|
|
# In case Mojolicious is not running behind a reverse proxy
|
|
|
|
|
$ip
|
|
|
|
|
//= sprintf( '%s:%s', $self->tx->remote_address, $self->tx->remote_port );
|
|
|
|
|
|
|
|
|
|
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
|
|
|
|
|
$self->render(
|
|
|
|
|
'register',
|
|
|
|
|
invalid => 'csrf',
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-20 20:52:02 +00:00
|
|
|
|
if ( my $registration_denylist
|
|
|
|
|
= $self->app->config->{registration}->{denylist} )
|
|
|
|
|
{
|
2021-10-30 08:03:40 +00:00
|
|
|
|
if ( open( my $fh, "<", $registration_denylist ) ) {
|
|
|
|
|
while ( my $line = <$fh> ) {
|
|
|
|
|
chomp $line;
|
|
|
|
|
if ( $ip eq $line ) {
|
|
|
|
|
close($fh);
|
|
|
|
|
$self->render( 'register', invalid => "denylist" );
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-08-20 20:52:02 +00:00
|
|
|
|
}
|
2021-10-30 08:03:40 +00:00
|
|
|
|
close($fh);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$self->log->error("Cannot open($registration_denylist): $!");
|
|
|
|
|
die("Cannot verify registration: $!");
|
2021-08-20 20:52:02 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-13 06:48:08 +00:00
|
|
|
|
if ( my $error = $self->users->is_name_invalid( name => $user ) ) {
|
|
|
|
|
$self->render( 'register', invalid => $error );
|
2019-03-22 15:56:49 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( not length($email) ) {
|
|
|
|
|
$self->render( 'register', invalid => 'mail_empty' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-13 04:20:21 +00:00
|
|
|
|
if ( $self->users->mail_is_blacklisted( email => $email ) ) {
|
2019-03-22 15:56:49 +00:00
|
|
|
|
$self->render( 'register', invalid => 'mail_blacklisted' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $password ne $password2 ) {
|
|
|
|
|
$self->render( 'register', invalid => 'password_notequal' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( length($password) < 8 ) {
|
|
|
|
|
$self->render( 'register', invalid => 'password_short' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-20 19:59:17 +00:00
|
|
|
|
if ( not $dt
|
|
|
|
|
or DateTime->now( time_zone => 'Europe/Berlin' )->epoch - $dt < 6 )
|
|
|
|
|
{
|
|
|
|
|
# a human user should take at least five seconds to fill out the form.
|
|
|
|
|
# Throw a CSRF error at presumed spammers.
|
|
|
|
|
$self->render(
|
|
|
|
|
'register',
|
|
|
|
|
invalid => 'csrf',
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-27 16:53:22 +00:00
|
|
|
|
my $token = make_token();
|
|
|
|
|
my $pw_hash = hash_password($password);
|
|
|
|
|
my $db = $self->pg->db;
|
|
|
|
|
my $tx = $db->begin;
|
2023-01-22 11:44:19 +00:00
|
|
|
|
my $user_id = $self->users->add(
|
2020-07-27 16:53:22 +00:00
|
|
|
|
db => $db,
|
|
|
|
|
name => $user,
|
|
|
|
|
email => $email,
|
|
|
|
|
token => $token,
|
|
|
|
|
password_hash => $pw_hash
|
|
|
|
|
);
|
2022-01-19 18:25:03 +00:00
|
|
|
|
|
|
|
|
|
my $success = $self->send_registration_mail(
|
|
|
|
|
email => $email,
|
|
|
|
|
token => $token,
|
|
|
|
|
ip => $ip,
|
|
|
|
|
user => $user,
|
|
|
|
|
user_id => $user_id
|
|
|
|
|
);
|
|
|
|
|
if ($success) {
|
|
|
|
|
$tx->commit;
|
|
|
|
|
$self->render( 'login', from => 'register' );
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$self->render( 'register', invalid => 'sendmail' );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-22 15:56:49 +00:00
|
|
|
|
sub verify {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
|
|
|
|
|
my $id = $self->stash('id');
|
|
|
|
|
my $token = $self->stash('token');
|
|
|
|
|
|
2019-05-02 09:34:52 +00:00
|
|
|
|
if ( not $id =~ m{ ^ \d+ $ }x or $id > 2147483647 ) {
|
2019-03-22 15:56:49 +00:00
|
|
|
|
$self->render( 'register', invalid => 'token' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-27 16:53:22 +00:00
|
|
|
|
if (
|
|
|
|
|
not $self->users->verify_registration_token(
|
|
|
|
|
uid => $id,
|
|
|
|
|
token => $token
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
{
|
2019-03-22 15:56:49 +00:00
|
|
|
|
$self->render( 'register', invalid => 'token' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
2019-04-30 16:05:07 +00:00
|
|
|
|
|
2019-03-22 15:56:49 +00:00
|
|
|
|
$self->render( 'login', from => 'verification' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub delete {
|
|
|
|
|
my ($self) = @_;
|
2023-01-22 10:34:53 +00:00
|
|
|
|
my $uid = $self->current_user->{id};
|
2019-03-22 15:56:49 +00:00
|
|
|
|
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
|
2023-06-04 16:21:36 +00:00
|
|
|
|
$self->flash( invalid => 'csrf' );
|
|
|
|
|
$self->redirect_to('account');
|
2019-03-22 15:56:49 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $self->param('action') eq 'delete' ) {
|
|
|
|
|
if (
|
|
|
|
|
not $self->authenticate(
|
|
|
|
|
$self->current_user->{name},
|
|
|
|
|
$self->param('password')
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
{
|
2023-06-04 16:21:36 +00:00
|
|
|
|
$self->flash( invalid => 'deletion password' );
|
|
|
|
|
$self->redirect_to('account');
|
2019-03-22 15:56:49 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
2023-01-22 10:34:53 +00:00
|
|
|
|
$self->users->flag_deletion( uid => $uid );
|
2019-03-22 15:56:49 +00:00
|
|
|
|
}
|
|
|
|
|
else {
|
2023-01-22 10:34:53 +00:00
|
|
|
|
$self->users->unflag_deletion( uid => $uid );
|
2019-03-22 15:56:49 +00:00
|
|
|
|
}
|
|
|
|
|
$self->redirect_to('account');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub do_logout {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
|
|
|
|
|
$self->render( 'login', invalid => 'csrf' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
$self->logout;
|
|
|
|
|
$self->redirect_to('/login');
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-30 21:23:49 +00:00
|
|
|
|
sub privacy {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
|
|
|
|
|
my $user = $self->current_user;
|
|
|
|
|
my $public_level = $user->{is_public};
|
|
|
|
|
|
|
|
|
|
if ( $self->param('action') and $self->param('action') eq 'save' ) {
|
2023-02-27 21:14:54 +00:00
|
|
|
|
my %opt;
|
|
|
|
|
my $default_visibility
|
|
|
|
|
= $visibility_atoi{ $self->param('status_level') };
|
|
|
|
|
if ( defined $default_visibility ) {
|
|
|
|
|
$opt{default_visibility} = $default_visibility;
|
2019-04-30 21:23:49 +00:00
|
|
|
|
}
|
2019-12-08 09:32:22 +00:00
|
|
|
|
|
2023-02-27 21:14:54 +00:00
|
|
|
|
$opt{comments_visible} = $self->param('public_comment') ? 1 : 0;
|
|
|
|
|
|
2023-03-04 08:03:50 +00:00
|
|
|
|
$opt{past_all} = $self->param('history_age') eq 'infinite' ? 1 : 0;
|
|
|
|
|
$opt{past_status} = $self->param('past_status') ? 1 : 0;
|
2020-02-14 15:29:44 +00:00
|
|
|
|
|
|
|
|
|
if ( $self->param('history_level') eq 'intern' ) {
|
2023-02-27 21:14:54 +00:00
|
|
|
|
$opt{past_visible} = 1;
|
2020-02-14 15:29:44 +00:00
|
|
|
|
}
|
|
|
|
|
elsif ( $self->param('history_level') eq 'extern' ) {
|
2023-02-27 21:14:54 +00:00
|
|
|
|
$opt{past_visible} = 2;
|
2020-02-14 15:29:44 +00:00
|
|
|
|
}
|
|
|
|
|
else {
|
2023-02-27 21:14:54 +00:00
|
|
|
|
$opt{past_visible} = 0;
|
2020-09-04 16:38:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-27 16:53:22 +00:00
|
|
|
|
$self->users->set_privacy(
|
2023-02-27 21:14:54 +00:00
|
|
|
|
uid => $user->{id},
|
|
|
|
|
%opt
|
2020-07-27 16:53:22 +00:00
|
|
|
|
);
|
2019-05-10 17:03:18 +00:00
|
|
|
|
|
|
|
|
|
$self->flash( success => 'privacy' );
|
2019-05-02 08:05:15 +00:00
|
|
|
|
$self->redirect_to('account');
|
2019-04-30 21:23:49 +00:00
|
|
|
|
}
|
|
|
|
|
else {
|
2019-12-08 10:06:17 +00:00
|
|
|
|
$self->param(
|
2023-02-27 21:14:54 +00:00
|
|
|
|
status_level => $visibility_itoa{ $user->{default_visibility} } );
|
|
|
|
|
$self->param( public_comment => $user->{comments_visible} );
|
2020-02-14 15:29:44 +00:00
|
|
|
|
$self->param(
|
2023-02-27 21:14:54 +00:00
|
|
|
|
history_level => $user->{past_visible} & 0x01 ? 'intern'
|
|
|
|
|
: $user->{past_visible} & 0x02 ? 'extern'
|
|
|
|
|
: 'private'
|
2020-02-14 15:29:44 +00:00
|
|
|
|
);
|
2023-02-27 21:14:54 +00:00
|
|
|
|
$self->param( history_age => $user->{past_all} ? 'infinite' : 'month' );
|
2023-03-04 08:03:50 +00:00
|
|
|
|
$self->param( past_status => $user->{past_status} );
|
2019-05-02 08:05:15 +00:00
|
|
|
|
$self->render( 'privacy', name => $user->{name} );
|
2019-04-30 21:23:49 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-04 16:21:36 +00:00
|
|
|
|
sub social {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
|
|
|
|
|
my $user = $self->current_user;
|
|
|
|
|
|
|
|
|
|
if ( $self->param('action') and $self->param('action') eq 'save' ) {
|
|
|
|
|
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
|
|
|
|
|
$self->render(
|
|
|
|
|
'social',
|
|
|
|
|
invalid => 'csrf',
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my %opt;
|
|
|
|
|
my $accept_follow = $self->param('accept_follow');
|
|
|
|
|
|
|
|
|
|
if ( $accept_follow eq 'yes' ) {
|
|
|
|
|
$opt{accept_follows} = 1;
|
|
|
|
|
}
|
|
|
|
|
elsif ( $accept_follow eq 'request' ) {
|
|
|
|
|
$opt{accept_follow_requests} = 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$self->users->set_social(
|
|
|
|
|
uid => $user->{id},
|
|
|
|
|
%opt
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$self->flash( success => 'social' );
|
|
|
|
|
$self->redirect_to('account');
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
if ( $user->{accept_follows} ) {
|
|
|
|
|
$self->param( accept_follow => 'yes' );
|
|
|
|
|
}
|
|
|
|
|
elsif ( $user->{accept_follow_requests} ) {
|
|
|
|
|
$self->param( accept_follow => 'request' );
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$self->param( accept_follow => 'no' );
|
|
|
|
|
}
|
|
|
|
|
$self->render( 'social', name => $user->{name} );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub social_list {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
|
|
|
|
|
my $kind = $self->stash('kind');
|
|
|
|
|
my $user = $self->current_user;
|
|
|
|
|
|
|
|
|
|
if ( $kind eq 'follow-requests' ) {
|
|
|
|
|
my @follow_reqs
|
|
|
|
|
= $self->users->get_follow_requests( uid => $user->{id} );
|
|
|
|
|
$self->render(
|
|
|
|
|
'social_list',
|
|
|
|
|
type => 'follow-requests',
|
|
|
|
|
entries => [@follow_reqs],
|
|
|
|
|
notifications => $user->{notifications},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
elsif ( $kind eq 'followers' ) {
|
|
|
|
|
my @followers = $self->users->get_followers( uid => $user->{id} );
|
|
|
|
|
$self->render(
|
|
|
|
|
'social_list',
|
|
|
|
|
type => 'followers',
|
|
|
|
|
entries => [@followers],
|
|
|
|
|
notifications => $user->{notifications},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
elsif ( $kind eq 'follows' ) {
|
|
|
|
|
my @following = $self->users->get_followees( uid => $user->{id} );
|
|
|
|
|
$self->render(
|
|
|
|
|
'social_list',
|
|
|
|
|
type => 'follows',
|
|
|
|
|
entries => [@following],
|
|
|
|
|
notifications => $user->{notifications},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
elsif ( $kind eq 'blocks' ) {
|
|
|
|
|
my @blocked = $self->users->get_blocked_users( uid => $user->{id} );
|
|
|
|
|
$self->render(
|
|
|
|
|
'social_list',
|
|
|
|
|
type => 'blocks',
|
|
|
|
|
entries => [@blocked],
|
|
|
|
|
notifications => $user->{notifications},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$self->render( 'not_found', status => 404 );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub social_action {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
|
|
|
|
|
my $user = $self->current_user;
|
|
|
|
|
my $action = $self->param('action');
|
|
|
|
|
my $target_ids = $self->param('target');
|
|
|
|
|
my $redirect_to = $self->param('redirect_to');
|
|
|
|
|
|
|
|
|
|
for my $key (
|
|
|
|
|
qw(follow request_follow follow_or_request unfollow remove_follower cancel_follow_request accept_follow_request reject_follow_request block unblock)
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
if ( $self->param($key) ) {
|
|
|
|
|
$action = $key;
|
|
|
|
|
$target_ids = $self->param($key);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
|
|
|
|
|
$self->redirect_to('/');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $action and $action eq 'clear_notifications' ) {
|
|
|
|
|
$self->users->update_notifications(
|
|
|
|
|
db => $self->pg->db,
|
|
|
|
|
uid => $user->{id},
|
|
|
|
|
has_follow_requests => 0
|
|
|
|
|
);
|
|
|
|
|
$self->flash( success => 'clear_notifications' );
|
|
|
|
|
$self->redirect_to('account');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( not( $action and $target_ids and $redirect_to ) ) {
|
|
|
|
|
$self->redirect_to('/');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for my $target_id ( split( qr{,}, $target_ids ) ) {
|
|
|
|
|
my $target = $self->users->get_privacy_by( uid => $target_id );
|
|
|
|
|
|
|
|
|
|
if ( not $target ) {
|
|
|
|
|
next;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $action eq 'follow' and $target->{accept_follows} ) {
|
|
|
|
|
$self->users->follow(
|
|
|
|
|
uid => $user->{id},
|
|
|
|
|
target => $target->{id}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
elsif ( $action eq 'request_follow'
|
|
|
|
|
and $target->{accept_follow_requests} )
|
|
|
|
|
{
|
|
|
|
|
$self->users->request_follow(
|
|
|
|
|
uid => $user->{id},
|
|
|
|
|
target => $target->{id}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
elsif ( $action eq 'follow_or_request' ) {
|
|
|
|
|
if ( $target->{accept_follows} ) {
|
|
|
|
|
$self->users->follow(
|
|
|
|
|
uid => $user->{id},
|
|
|
|
|
target => $target->{id}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
elsif ( $target->{accept_follow_requests} ) {
|
|
|
|
|
$self->users->request_follow(
|
|
|
|
|
uid => $user->{id},
|
|
|
|
|
target => $target->{id}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
elsif ( $action eq 'unfollow' ) {
|
|
|
|
|
$self->users->unfollow(
|
|
|
|
|
uid => $user->{id},
|
|
|
|
|
target => $target->{id}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
elsif ( $action eq 'remove_follower' ) {
|
|
|
|
|
$self->users->remove_follower(
|
|
|
|
|
uid => $user->{id},
|
|
|
|
|
follower => $target->{id}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
elsif ( $action eq 'cancel_follow_request' ) {
|
|
|
|
|
$self->users->cancel_follow_request(
|
|
|
|
|
uid => $user->{id},
|
|
|
|
|
target => $target->{id}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
elsif ( $action eq 'accept_follow_request' ) {
|
|
|
|
|
$self->users->accept_follow_request(
|
|
|
|
|
uid => $user->{id},
|
|
|
|
|
applicant => $target->{id}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
elsif ( $action eq 'reject_follow_request' ) {
|
|
|
|
|
$self->users->reject_follow_request(
|
|
|
|
|
uid => $user->{id},
|
|
|
|
|
applicant => $target->{id}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
elsif ( $action eq 'block' ) {
|
|
|
|
|
$self->users->block(
|
|
|
|
|
uid => $user->{id},
|
|
|
|
|
target => $target->{id}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
elsif ( $action eq 'unblock' ) {
|
|
|
|
|
$self->users->unblock(
|
|
|
|
|
uid => $user->{id},
|
|
|
|
|
target => $target->{id}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $redirect_to eq 'profile' ) {
|
|
|
|
|
|
|
|
|
|
# profile links do not perform bulk actions
|
|
|
|
|
$self->redirect_to( '/p/' . $target->{name} );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$self->redirect_to($redirect_to);
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-04 12:28:04 +00:00
|
|
|
|
sub profile {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
my $user = $self->current_user;
|
|
|
|
|
|
|
|
|
|
if ( $self->param('action') and $self->param('action') eq 'save' ) {
|
|
|
|
|
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
|
|
|
|
|
$self->render(
|
|
|
|
|
'edit_profile',
|
|
|
|
|
invalid => 'csrf',
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
my $md = Text::Markdown->new;
|
|
|
|
|
my $bio = $self->param('bio');
|
|
|
|
|
|
|
|
|
|
if ( length($bio) > 2000 ) {
|
|
|
|
|
$bio = substr( $bio, 0, 2000 ) . '…';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $profile = {
|
|
|
|
|
bio => {
|
|
|
|
|
markdown => $bio,
|
|
|
|
|
html => $md->markdown( xml_escape($bio) ),
|
|
|
|
|
},
|
|
|
|
|
metadata => [],
|
|
|
|
|
};
|
|
|
|
|
for my $i ( 0 .. 20 ) {
|
|
|
|
|
my $key = $self->param("key_$i");
|
|
|
|
|
my $value = $self->param("value_$i");
|
|
|
|
|
if ($key) {
|
|
|
|
|
if ( length($value) > 500 ) {
|
|
|
|
|
$value = substr( $value, 0, 500 ) . '…';
|
|
|
|
|
}
|
|
|
|
|
my $html_value
|
|
|
|
|
= ( $value
|
2023-06-04 16:56:43 +00:00
|
|
|
|
=~ s{ \[ ([^]]+) \]\( ([^)]+) \) }{'<a href="' . xml_escape($2) . '" rel="me">' . xml_escape($1) .'</a>' }egrx
|
2023-06-04 12:28:04 +00:00
|
|
|
|
);
|
|
|
|
|
$profile->{metadata}[$i] = {
|
|
|
|
|
key => $key,
|
|
|
|
|
value => {
|
|
|
|
|
markdown => $value,
|
|
|
|
|
html => $html_value,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
last;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$self->users->set_profile(
|
|
|
|
|
uid => $user->{id},
|
|
|
|
|
profile => $profile
|
|
|
|
|
);
|
|
|
|
|
$self->redirect_to( '/p/' . $user->{name} );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $profile = $self->users->get_profile( uid => $user->{id} );
|
|
|
|
|
$self->param( bio => $profile->{bio}{markdown} );
|
|
|
|
|
for my $i ( 0 .. $#{ $profile->{metadata} } ) {
|
|
|
|
|
$self->param( "key_$i" => $profile->{metadata}[$i]{key} );
|
|
|
|
|
$self->param( "value_$i" => $profile->{metadata}[$i]{value}{markdown} );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$self->render( 'edit_profile', name => $user->{name} );
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-20 17:15:21 +00:00
|
|
|
|
sub insight {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
|
2022-09-24 16:53:04 +00:00
|
|
|
|
my $user = $self->current_user;
|
|
|
|
|
my ( $use_history, $destinations ) = $self->users->use_history(
|
|
|
|
|
uid => $user->{id},
|
|
|
|
|
with_local_transit => 1
|
|
|
|
|
);
|
2019-05-20 17:15:21 +00:00
|
|
|
|
|
|
|
|
|
if ( $self->param('action') and $self->param('action') eq 'save' ) {
|
|
|
|
|
if ( $self->param('on_departure') ) {
|
|
|
|
|
$use_history |= 0x01;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$use_history &= ~0x01;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $self->param('on_arrival') ) {
|
|
|
|
|
$use_history |= 0x02;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$use_history &= ~0x02;
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-24 16:53:04 +00:00
|
|
|
|
if ( $self->param('local_transit') ) {
|
|
|
|
|
$use_history |= 0x04;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$use_history &= ~0x04;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $self->param('destinations') ) {
|
|
|
|
|
$destinations
|
|
|
|
|
= [ split( qr{\r?\n\r?}, $self->param('destinations') ) ];
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-27 16:53:22 +00:00
|
|
|
|
$self->users->use_history(
|
2022-09-24 16:53:04 +00:00
|
|
|
|
uid => $user->{id},
|
|
|
|
|
set => $use_history,
|
|
|
|
|
destinations => $destinations
|
2020-07-27 16:53:22 +00:00
|
|
|
|
);
|
2019-05-20 17:15:21 +00:00
|
|
|
|
$self->flash( success => 'use_history' );
|
|
|
|
|
$self->redirect_to('account');
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-24 16:53:04 +00:00
|
|
|
|
$self->param( on_departure => $use_history & 0x01 ? 1 : 0 );
|
|
|
|
|
$self->param( on_arrival => $use_history & 0x02 ? 1 : 0 );
|
|
|
|
|
$self->param( local_transit => $use_history & 0x04 ? 1 : 0 );
|
|
|
|
|
$self->param( destinations => join( "\n", @{$destinations} ) );
|
2019-05-20 17:15:21 +00:00
|
|
|
|
$self->render('use_history');
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-11 20:09:26 +00:00
|
|
|
|
sub services {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
my $user = $self->current_user;
|
|
|
|
|
|
|
|
|
|
if ( $self->param('action') and $self->param('action') eq 'save' ) {
|
|
|
|
|
my $sb = $self->param('stationboard');
|
|
|
|
|
my $value = 0;
|
2022-07-16 11:55:36 +00:00
|
|
|
|
if ( $sb =~ m{ ^ \d+ $ }x and $sb >= 0 and $sb <= 4 ) {
|
2022-07-11 20:09:26 +00:00
|
|
|
|
$value = int($sb);
|
|
|
|
|
}
|
|
|
|
|
$self->users->use_external_services(
|
|
|
|
|
uid => $user->{id},
|
|
|
|
|
set => $value
|
|
|
|
|
);
|
|
|
|
|
$self->flash( success => 'external' );
|
|
|
|
|
$self->redirect_to('account');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$self->param( stationboard =>
|
|
|
|
|
$self->users->use_external_services( uid => $user->{id} ) );
|
|
|
|
|
$self->render('use_external_links');
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-05 16:09:11 +00:00
|
|
|
|
sub webhook {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
|
2022-02-20 16:35:30 +00:00
|
|
|
|
my $uid = $self->current_user->{id};
|
|
|
|
|
|
|
|
|
|
my $hook = $self->users->get_webhook( uid => $uid );
|
2019-05-05 16:09:11 +00:00
|
|
|
|
|
|
|
|
|
if ( $self->param('action') and $self->param('action') eq 'save' ) {
|
|
|
|
|
$hook->{url} = $self->param('url');
|
|
|
|
|
$hook->{token} = $self->param('token');
|
|
|
|
|
$hook->{enabled} = $self->param('enabled') // 0;
|
2022-02-20 16:35:30 +00:00
|
|
|
|
$self->users->set_webhook(
|
|
|
|
|
uid => $uid,
|
2019-05-05 16:09:11 +00:00
|
|
|
|
url => $hook->{url},
|
|
|
|
|
token => $hook->{token},
|
|
|
|
|
enabled => $hook->{enabled}
|
|
|
|
|
);
|
2019-05-14 18:05:45 +00:00
|
|
|
|
$self->run_hook(
|
|
|
|
|
$self->current_user->{id},
|
|
|
|
|
'ping',
|
|
|
|
|
sub {
|
|
|
|
|
$self->render(
|
|
|
|
|
'webhooks',
|
2022-02-20 16:35:30 +00:00
|
|
|
|
hook => $self->users->get_webhook( uid => $uid ),
|
2019-05-14 18:05:45 +00:00
|
|
|
|
new_hook => 1
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
return;
|
2019-05-05 16:09:11 +00:00
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$self->param( url => $hook->{url} );
|
|
|
|
|
$self->param( token => $hook->{token} );
|
|
|
|
|
$self->param( enabled => $hook->{enabled} );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$self->render( 'webhooks', hook => $hook );
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-30 10:08:51 +00:00
|
|
|
|
sub change_mail {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
|
|
|
|
|
my $action = $self->req->param('action');
|
|
|
|
|
my $password = $self->req->param('password');
|
|
|
|
|
my $email = $self->req->param('email');
|
|
|
|
|
|
|
|
|
|
if ( $action and $action eq 'update_mail' ) {
|
|
|
|
|
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
|
|
|
|
|
$self->render(
|
|
|
|
|
'change_mail',
|
|
|
|
|
invalid => 'csrf',
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( not length($email) ) {
|
|
|
|
|
$self->render( 'change_mail', invalid => 'mail_empty' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
not $self->authenticate(
|
|
|
|
|
$self->current_user->{name},
|
|
|
|
|
$self->param('password')
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
$self->render( 'change_mail', invalid => 'password' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $token = make_token();
|
|
|
|
|
my $db = $self->pg->db;
|
|
|
|
|
my $tx = $db->begin;
|
|
|
|
|
|
2020-07-27 16:53:22 +00:00
|
|
|
|
$self->users->mark_for_mail_change(
|
|
|
|
|
db => $db,
|
|
|
|
|
uid => $self->current_user->{id},
|
|
|
|
|
email => $email,
|
|
|
|
|
token => $token
|
|
|
|
|
);
|
2019-04-30 10:08:51 +00:00
|
|
|
|
|
2022-01-19 18:25:03 +00:00
|
|
|
|
my $success = $self->send_address_confirmation_mail( $email, $token );
|
2019-04-30 10:08:51 +00:00
|
|
|
|
|
|
|
|
|
if ($success) {
|
|
|
|
|
$tx->commit;
|
|
|
|
|
$self->render( 'change_mail', success => 1 );
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$self->render( 'change_mail', invalid => 'sendmail' );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$self->render('change_mail');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-12 17:00:42 +00:00
|
|
|
|
sub change_name {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
|
|
|
|
|
my $action = $self->req->param('action');
|
|
|
|
|
my $password = $self->req->param('password');
|
|
|
|
|
my $old_name = $self->current_user->{name};
|
|
|
|
|
my $new_name = $self->req->param('name');
|
|
|
|
|
|
|
|
|
|
if ( $action and $action eq 'update_name' ) {
|
|
|
|
|
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
|
|
|
|
|
$self->render(
|
|
|
|
|
'change_name',
|
2021-06-14 17:30:58 +00:00
|
|
|
|
name => $old_name,
|
2021-06-12 17:00:42 +00:00
|
|
|
|
invalid => 'csrf',
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-13 06:48:08 +00:00
|
|
|
|
if ( my $error = $self->users->is_name_invalid( name => $new_name ) ) {
|
2021-06-14 17:30:58 +00:00
|
|
|
|
$self->render(
|
|
|
|
|
'change_name',
|
|
|
|
|
name => $old_name,
|
|
|
|
|
invalid => $error
|
|
|
|
|
);
|
2021-06-12 17:00:42 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( not $self->authenticate( $old_name, $self->param('password') ) ) {
|
2021-06-14 17:30:58 +00:00
|
|
|
|
$self->render(
|
|
|
|
|
'change_name',
|
|
|
|
|
name => $old_name,
|
|
|
|
|
invalid => 'password'
|
|
|
|
|
);
|
2021-06-12 17:00:42 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-13 06:48:08 +00:00
|
|
|
|
# The users table has a unique constraint on the "name" column, so having
|
|
|
|
|
# two users with the same name is not possible. The race condition
|
|
|
|
|
# between the user_name_exists check in is_name_invalid and this
|
|
|
|
|
# change_name call is harmless.
|
2021-06-12 17:00:42 +00:00
|
|
|
|
my $success = $self->users->change_name(
|
|
|
|
|
uid => $self->current_user->{id},
|
|
|
|
|
name => $new_name
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if ( not $success ) {
|
2021-06-14 17:30:58 +00:00
|
|
|
|
$self->render(
|
|
|
|
|
'change_name',
|
|
|
|
|
name => $old_name,
|
|
|
|
|
invalid => 'user_collision'
|
|
|
|
|
);
|
2021-06-12 17:00:42 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$self->flash( success => 'name' );
|
|
|
|
|
$self->redirect_to('account');
|
|
|
|
|
|
2022-01-19 18:25:03 +00:00
|
|
|
|
$self->send_name_notification_mail( $old_name, $new_name );
|
2021-06-12 17:00:42 +00:00
|
|
|
|
}
|
|
|
|
|
else {
|
2021-06-14 17:30:58 +00:00
|
|
|
|
$self->render( 'change_name', name => $old_name );
|
2021-06-12 17:00:42 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-06 09:20:10 +00:00
|
|
|
|
sub password_form {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
|
|
|
|
|
$self->render('change_password');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub change_password {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
my $old_password = $self->req->param('oldpw');
|
|
|
|
|
my $password = $self->req->param('newpw');
|
|
|
|
|
my $password2 = $self->req->param('newpw2');
|
|
|
|
|
|
|
|
|
|
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
|
|
|
|
|
$self->render( 'change_password', invalid => 'csrf' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $password ne $password2 ) {
|
|
|
|
|
$self->render( 'change_password', invalid => 'password_notequal' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( length($password) < 8 ) {
|
|
|
|
|
$self->render( 'change_password', invalid => 'password_short' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
not $self->authenticate(
|
|
|
|
|
$self->current_user->{name},
|
|
|
|
|
$self->param('oldpw')
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
$self->render( 'change_password', invalid => 'password' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $pw_hash = hash_password($password);
|
2020-07-27 16:53:22 +00:00
|
|
|
|
$self->users->set_password_hash(
|
|
|
|
|
uid => $self->current_user->{id},
|
|
|
|
|
password_hash => $pw_hash
|
|
|
|
|
);
|
2019-04-06 09:20:10 +00:00
|
|
|
|
|
2019-04-30 10:08:51 +00:00
|
|
|
|
$self->flash( success => 'password' );
|
2019-04-06 09:20:10 +00:00
|
|
|
|
$self->redirect_to('account');
|
2022-01-19 18:25:03 +00:00
|
|
|
|
$self->send_password_notification_mail();
|
|
|
|
|
}
|
2019-04-06 09:20:10 +00:00
|
|
|
|
|
2019-04-29 18:12:59 +00:00
|
|
|
|
sub request_password_reset {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
|
|
|
|
|
if ( $self->param('action') and $self->param('action') eq 'initiate' ) {
|
|
|
|
|
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
|
|
|
|
|
$self->render( 'recover_password', invalid => 'csrf' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $name = $self->param('user');
|
|
|
|
|
my $email = $self->param('email');
|
|
|
|
|
|
2020-07-27 16:53:22 +00:00
|
|
|
|
my $uid = $self->users->get_uid_by_name_and_mail(
|
|
|
|
|
name => $name,
|
|
|
|
|
email => $email
|
|
|
|
|
);
|
2019-04-29 18:12:59 +00:00
|
|
|
|
|
|
|
|
|
if ( not $uid ) {
|
2019-04-30 08:46:46 +00:00
|
|
|
|
$self->render( 'recover_password',
|
|
|
|
|
invalid => 'recovery credentials' );
|
2019-04-29 18:12:59 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $token = make_token();
|
|
|
|
|
my $db = $self->pg->db;
|
|
|
|
|
my $tx = $db->begin;
|
|
|
|
|
|
2020-07-27 16:53:22 +00:00
|
|
|
|
my $error = $self->users->mark_for_password_reset(
|
|
|
|
|
db => $db,
|
|
|
|
|
uid => $uid,
|
|
|
|
|
token => $token
|
|
|
|
|
);
|
2019-04-29 18:12:59 +00:00
|
|
|
|
|
|
|
|
|
if ($error) {
|
|
|
|
|
$self->render( 'recover_password', invalid => $error );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-19 18:25:03 +00:00
|
|
|
|
my $success = $self->send_lostpassword_confirmation_mail(
|
|
|
|
|
email => $email,
|
|
|
|
|
name => $name,
|
|
|
|
|
uid => $uid,
|
|
|
|
|
token => $token
|
|
|
|
|
);
|
2019-04-29 18:12:59 +00:00
|
|
|
|
|
|
|
|
|
if ($success) {
|
|
|
|
|
$tx->commit;
|
|
|
|
|
$self->render( 'recover_password', success => 1 );
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$self->render( 'recover_password', invalid => 'sendmail' );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
elsif ( $self->param('action')
|
|
|
|
|
and $self->param('action') eq 'set_password' )
|
|
|
|
|
{
|
|
|
|
|
my $id = $self->param('id');
|
|
|
|
|
my $token = $self->param('token');
|
|
|
|
|
my $password = $self->param('newpw');
|
|
|
|
|
my $password2 = $self->param('newpw2');
|
|
|
|
|
|
|
|
|
|
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
|
|
|
|
|
$self->render( 'set_password', invalid => 'csrf' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-07-27 16:53:22 +00:00
|
|
|
|
if (
|
|
|
|
|
not $self->users->verify_password_token(
|
|
|
|
|
uid => $id,
|
|
|
|
|
token => $token
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
{
|
2019-04-30 10:08:51 +00:00
|
|
|
|
$self->render( 'recover_password', invalid => 'change token' );
|
2019-04-29 18:12:59 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if ( $password ne $password2 ) {
|
|
|
|
|
$self->render( 'set_password', invalid => 'password_notequal' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( length($password) < 8 ) {
|
|
|
|
|
$self->render( 'set_password', invalid => 'password_short' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $pw_hash = hash_password($password);
|
2020-07-27 16:53:22 +00:00
|
|
|
|
$self->users->set_password_hash(
|
|
|
|
|
uid => $id,
|
|
|
|
|
password_hash => $pw_hash
|
|
|
|
|
);
|
2019-04-29 18:12:59 +00:00
|
|
|
|
|
|
|
|
|
my $account = $self->get_user_data($id);
|
|
|
|
|
|
|
|
|
|
if ( not $self->authenticate( $account->{name}, $password ) ) {
|
|
|
|
|
$self->render( 'set_password',
|
|
|
|
|
invalid => 'Authentication failure – WTF?' );
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-30 10:08:51 +00:00
|
|
|
|
$self->flash( success => 'password' );
|
2019-04-29 18:12:59 +00:00
|
|
|
|
$self->redirect_to('account');
|
|
|
|
|
|
2020-07-27 16:53:22 +00:00
|
|
|
|
$self->users->remove_password_token(
|
|
|
|
|
uid => $id,
|
|
|
|
|
token => $token
|
|
|
|
|
);
|
2019-04-29 18:12:59 +00:00
|
|
|
|
|
2022-01-19 18:25:03 +00:00
|
|
|
|
$self->send_lostpassword_notification_mail($account);
|
2019-04-29 18:12:59 +00:00
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$self->render('recover_password');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub recover_password {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
|
|
|
|
|
my $id = $self->stash('id');
|
|
|
|
|
my $token = $self->stash('token');
|
|
|
|
|
|
2019-05-02 09:34:52 +00:00
|
|
|
|
if ( not $id =~ m{ ^ \d+ $ }x or $id > 2147483647 ) {
|
|
|
|
|
$self->render( 'recover_password', invalid => 'recovery token' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-27 16:53:22 +00:00
|
|
|
|
if (
|
|
|
|
|
$self->users->verify_password_token(
|
|
|
|
|
uid => $id,
|
|
|
|
|
token => $token
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
{
|
2019-04-29 18:12:59 +00:00
|
|
|
|
$self->render('set_password');
|
|
|
|
|
}
|
|
|
|
|
else {
|
2019-04-30 08:46:46 +00:00
|
|
|
|
$self->render( 'recover_password', invalid => 'recovery token' );
|
2019-04-29 18:12:59 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-30 10:08:51 +00:00
|
|
|
|
sub confirm_mail {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
my $id = $self->current_user->{id};
|
|
|
|
|
my $token = $self->stash('token');
|
|
|
|
|
|
2020-07-27 16:53:22 +00:00
|
|
|
|
if (
|
|
|
|
|
$self->users->change_mail_with_token(
|
|
|
|
|
uid => $id,
|
|
|
|
|
token => $token
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
{
|
2019-04-30 10:08:51 +00:00
|
|
|
|
$self->flash( success => 'mail' );
|
|
|
|
|
$self->redirect_to('account');
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$self->render( 'change_mail', invalid => 'change token' );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-22 15:56:49 +00:00
|
|
|
|
sub account {
|
2023-06-04 16:21:36 +00:00
|
|
|
|
my ($self) = @_;
|
|
|
|
|
my $uid = $self->current_user->{id};
|
|
|
|
|
my $follow_requests = $self->users->has_follow_requests( uid => $uid );
|
|
|
|
|
my $followers = $self->users->has_followers( uid => $uid );
|
|
|
|
|
my $following = $self->users->has_followees( uid => $uid );
|
|
|
|
|
my $blocked = $self->users->has_blocked_users( uid => $uid );
|
2019-03-22 15:56:49 +00:00
|
|
|
|
|
2023-06-04 16:21:36 +00:00
|
|
|
|
$self->render(
|
|
|
|
|
'account',
|
|
|
|
|
api_token => $self->users->get_api_token( uid => $uid ),
|
|
|
|
|
num_follow_requests => $follow_requests,
|
|
|
|
|
num_followers => $followers,
|
|
|
|
|
num_following => $following,
|
|
|
|
|
num_blocked => $blocked,
|
|
|
|
|
);
|
2023-01-22 10:34:53 +00:00
|
|
|
|
$self->users->mark_seen( uid => $uid );
|
2019-03-22 15:56:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub json_export {
|
|
|
|
|
my ($self) = @_;
|
2019-04-23 16:08:07 +00:00
|
|
|
|
my $uid = $self->current_user->{id};
|
|
|
|
|
|
|
|
|
|
my $db = $self->pg->db;
|
2019-03-22 15:56:49 +00:00
|
|
|
|
|
|
|
|
|
$self->render(
|
2019-04-23 16:08:07 +00:00
|
|
|
|
json => {
|
2020-07-27 16:53:22 +00:00
|
|
|
|
account => $db->select( 'users', '*', { id => $uid } )->hash,
|
2019-04-24 05:13:38 +00:00
|
|
|
|
in_transit => [
|
2019-05-03 19:14:49 +00:00
|
|
|
|
$db->select( 'in_transit_str', '*', { user_id => $uid } )
|
2019-04-24 05:13:38 +00:00
|
|
|
|
->hashes->each
|
|
|
|
|
],
|
2019-04-23 16:08:07 +00:00
|
|
|
|
journeys => [
|
2019-05-03 19:14:49 +00:00
|
|
|
|
$db->select( 'journeys_str', '*', { user_id => $uid } )
|
2019-04-23 16:08:07 +00:00
|
|
|
|
->hashes->each
|
|
|
|
|
],
|
|
|
|
|
}
|
2019-03-22 15:56:49 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-08 08:40:49 +00:00
|
|
|
|
sub webfinger {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
|
|
|
|
|
my $resource = $self->param('resource');
|
|
|
|
|
|
|
|
|
|
if ( not $resource ) {
|
2023-01-08 08:58:58 +00:00
|
|
|
|
$self->render( 'not_found', status => 404 );
|
2023-01-08 08:40:49 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-08 08:58:58 +00:00
|
|
|
|
my $root_url = $self->base_url_for('/')->to_abs->host;
|
2023-01-08 08:40:49 +00:00
|
|
|
|
|
|
|
|
|
if ( not $root_url
|
2023-03-04 12:23:19 +00:00
|
|
|
|
or not $resource
|
|
|
|
|
=~ m{ ^ acct: [@]? (?<name> [^@]+ ) [@] $root_url $ }x )
|
2023-01-08 08:40:49 +00:00
|
|
|
|
{
|
2023-01-08 08:58:58 +00:00
|
|
|
|
$self->render( 'not_found', status => 404 );
|
2023-01-08 08:40:49 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $name = $+{name};
|
2023-05-24 19:36:06 +00:00
|
|
|
|
my $user = $self->users->get_privacy_by( name => $name );
|
2023-01-08 08:40:49 +00:00
|
|
|
|
|
2023-03-04 12:23:19 +00:00
|
|
|
|
if ( not $user ) {
|
2023-01-08 08:58:58 +00:00
|
|
|
|
$self->render( 'not_found', status => 404 );
|
2023-01-08 09:00:42 +00:00
|
|
|
|
return;
|
2023-01-08 08:40:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $profile_url
|
2023-01-08 08:58:58 +00:00
|
|
|
|
= $self->base_url_for("/p/${name}")->to_abs->scheme('https')->to_string;
|
2023-01-08 08:40:49 +00:00
|
|
|
|
|
|
|
|
|
$self->render(
|
|
|
|
|
text => JSON->new->encode(
|
|
|
|
|
{
|
2023-01-08 09:02:45 +00:00
|
|
|
|
subject => $resource,
|
2023-01-08 08:40:49 +00:00
|
|
|
|
aliases => [ $profile_url, ],
|
|
|
|
|
links => [
|
|
|
|
|
{
|
|
|
|
|
rel => 'http://webfinger.net/rel/profile-page',
|
|
|
|
|
type => 'text/html',
|
|
|
|
|
href => $profile_url,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
format => 'json',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-22 15:56:49 +00:00
|
|
|
|
1;
|