Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mobile): allow users to change contact details #1994

Merged
merged 3 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Fix(mobile): Make disclaimer screen usable on smaller devices.
- Feat(mobile): Allow to manually increase for how many derived addresses to sync for. This might be needed if you recovered a wallet and do not see any on-chain funds.
- Feat(mobile): Allow users to register with Email, Nostr and Telegram handle and let them change these details later on.

## [1.8.6] - 2024-02-08

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ALTER TABLE
users RENAME COLUMN contact TO email;
ALTER TABLE
users ADD COLUMN nostr TEXT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ALTER TABLE
users RENAME COLUMN email TO contact;
ALTER TABLE
users DROP COLUMN nostr;
4 changes: 2 additions & 2 deletions coordinator/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ pub async fn list_channels(
.map(|channel| {
let user_email =
match db::user::by_id(&mut conn, channel.counterparty.node_id.to_string()) {
Ok(Some(user)) => user.email,
Ok(Some(user)) => user.contact,
_ => "unknown".to_string(),
};
let balances = if let Some(funding_txo) = channel.funding_txo {
Expand Down Expand Up @@ -338,7 +338,7 @@ pub async fn list_dlc_channels(
.map(|dlc_channel| {
let (email, registration_timestamp) =
match db::user::by_id(&mut conn, dlc_channel.get_counter_party_id().to_string()) {
Ok(Some(user)) => (user.email, Some(user.timestamp)),
Ok(Some(user)) => (user.contact, Some(user.timestamp)),
_ => ("unknown".to_string(), None),
};

Expand Down
27 changes: 16 additions & 11 deletions coordinator/src/db/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ pub struct User {
#[diesel(deserialize_as = i32)]
pub id: Option<i32>,
pub pubkey: String,
pub email: String,
pub nostr: String,
pub contact: String,
pub timestamp: OffsetDateTime,
pub fcm_token: String,
pub last_login: OffsetDateTime,
Expand All @@ -27,8 +26,7 @@ impl From<RegisterParams> for User {
User {
id: None,
pubkey: value.pubkey.to_string(),
email: value.email.unwrap_or("".to_owned()),
nostr: value.nostr.unwrap_or("".to_owned()),
contact: value.contact.unwrap_or("".to_owned()),
timestamp: OffsetDateTime::now_utc(),
fcm_token: "".to_owned(),
last_login: OffsetDateTime::now_utc(),
Expand All @@ -48,26 +46,25 @@ pub fn by_id(conn: &mut PgConnection, id: String) -> QueryResult<Option<User>> {
Ok(x)
}

pub fn upsert_email(
pub fn upsert_user(
conn: &mut PgConnection,
trader_id: PublicKey,
email: String,
contact: String,
) -> QueryResult<User> {
let timestamp = OffsetDateTime::now_utc();

let user: User = diesel::insert_into(users::table)
.values(User {
id: None,
pubkey: trader_id.to_string(),
email: email.clone(),
nostr: "".to_owned(),
contact: contact.clone(),
timestamp,
fcm_token: "".to_owned(),
last_login: timestamp,
})
.on_conflict(schema::users::pubkey)
.do_update()
.set((users::email.eq(&email), users::last_login.eq(timestamp)))
.set((users::contact.eq(&contact), users::last_login.eq(timestamp)))
.get_result(conn)?;
Ok(user)
}
Expand All @@ -79,8 +76,7 @@ pub fn login_user(conn: &mut PgConnection, trader_id: PublicKey, token: String)
.values(User {
id: None,
pubkey: trader_id.to_string(),
email: "".to_owned(),
nostr: "".to_owned(),
contact: "".to_owned(),
timestamp: OffsetDateTime::now_utc(),
fcm_token: token.clone(),
last_login,
Expand All @@ -100,3 +96,12 @@ pub fn login_user(conn: &mut PgConnection, trader_id: PublicKey, token: String)
}
Ok(())
}

pub fn get_user(conn: &mut PgConnection, trader_id: PublicKey) -> Result<Option<User>> {
let maybe_user = users::table
.filter(users::pubkey.eq(trader_id.to_string()))
.first(conn)
.optional()?;

Ok(maybe_user)
}
5 changes: 2 additions & 3 deletions coordinator/src/orderbook/tests/registration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,16 @@ async fn registered_user_is_stored_in_db() {
let dummy_email = "dummy@user.com".to_string();
let fcm_token = "just_a_token".to_string();

let user = user::upsert_email(&mut conn, dummy_pubkey, dummy_email.clone()).unwrap();
let user = user::upsert_user(&mut conn, dummy_pubkey, dummy_email.clone()).unwrap();
assert!(user.id.is_some(), "Id should be filled in by diesel");
user::login_user(&mut conn, dummy_pubkey, fcm_token.clone()).unwrap();

let users = user::all(&mut conn).unwrap();
assert_eq!(users.len(), 1);
// We started without the id, so we can't compare the whole user.
assert_eq!(users.first().unwrap().pubkey, dummy_pubkey.to_string());
assert_eq!(users.first().unwrap().email, dummy_email);
assert_eq!(users.first().unwrap().contact, dummy_email);
assert_eq!(users.first().unwrap().fcm_token, fcm_token);
assert!(users.first().unwrap().nostr.is_empty());
}

fn dummy_public_key() -> PublicKey {
Expand Down
43 changes: 41 additions & 2 deletions coordinator/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::collaborative_revert::confirm_legacy_collaborative_revert;
use crate::db;
use crate::db::liquidity::LiquidityRequestLog;
use crate::db::user;
use crate::db::user::User;
use crate::is_liquidity_sufficient;
use crate::leaderboard::generate_leader_board;
use crate::leaderboard::LeaderBoard;
Expand Down Expand Up @@ -160,7 +161,11 @@ pub fn router(
.route("/api/orderbook/websocket", get(websocket_handler))
.route("/api/trade", post(post_trade))
.route("/api/rollover/:dlc_channel_id", post(rollover))
// Deprecated: we just keep it for backwards compatbility as otherwise old apps won't
// pass registration
.route("/api/register", post(post_register))
.route("/api/users", post(post_register))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, anybody could use this api to update the contact details of a user.

.route("/api/users/:trader_pubkey", get(get_user))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 That api is concerning for me. If somebody learns about a users node id they can use this api to query user data.

I propose we either validate a users signature on that api or rather put that information into the authenticated message received by the app after a successful login. (there we already verify the users signature)

.route("/api/admin/wallet/balance", get(get_balance))
.route("/api/admin/wallet/utxos", get(get_utxos))
.route("/api/admin/channels", get(list_channels).post(open_channel))
Expand Down Expand Up @@ -438,8 +443,8 @@ pub async fn post_register(
.get()
.map_err(|e| AppError::InternalServerError(format!("Could not get connection: {e:#}")))?;

if let Some(email) = register_params.email {
user::upsert_email(&mut conn, register_params.pubkey, email)
if let Some(contact) = register_params.contact {
user::upsert_user(&mut conn, register_params.pubkey, contact)
.map_err(|e| AppError::InternalServerError(format!("Could not upsert user: {e:#}")))?;
} else {
tracing::warn!(trader_id=%register_params.pubkey, "Did not receive an email during registration");
Expand All @@ -448,6 +453,40 @@ pub async fn post_register(
Ok(())
}

impl TryFrom<User> for commons::User {
type Error = AppError;
fn try_from(value: User) -> Result<Self, Self::Error> {
Ok(commons::User {
pubkey: PublicKey::from_str(&value.pubkey).map_err(|_| {
AppError::InternalServerError("Could not parse user pubkey".to_string())
})?,
contact: Some(value.contact).filter(|s| !s.is_empty()),
})
}
}

#[instrument(skip_all, err(Debug))]
pub async fn get_user(
State(state): State<Arc<AppState>>,
Path(trader_pubkey): Path<String>,
) -> Result<Json<commons::User>, AppError> {
let mut conn = state
.pool
.get()
.map_err(|e| AppError::InternalServerError(format!("Could not get connection: {e:#}")))?;

let trader_pubkey = PublicKey::from_str(trader_pubkey.as_str())
.map_err(|_| AppError::BadRequest("Invalid trader id provided".to_string()))?;

let option = user::get_user(&mut conn, trader_pubkey)
.map_err(|e| AppError::InternalServerError(format!("Could not load users: {e:#}")))?;

match option {
None => Err(AppError::NoMatchFound("No user found".to_string())),
Some(user) => Ok(Json(user.try_into()?)),
}
}

async fn get_settings(State(state): State<Arc<AppState>>) -> impl IntoResponse {
let settings = state.settings.read().await;
serde_json::to_string(&*settings).expect("to be able to serialise settings")
Expand Down
3 changes: 1 addition & 2 deletions coordinator/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,7 @@ diesel::table! {
users (id) {
id -> Int4,
pubkey -> Text,
email -> Text,
nostr -> Text,
contact -> Text,
timestamp -> Timestamptz,
fcm_token -> Text,
last_login -> Timestamptz,
Expand Down
9 changes: 7 additions & 2 deletions crates/commons/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ pub const AUTH_SIGN_MESSAGE: &[u8; 19] = b"Hello it's me Mario";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisterParams {
pub pubkey: PublicKey,
pub email: Option<String>,
pub nostr: Option<String>,
pub contact: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub pubkey: PublicKey,
pub contact: Option<String>,
}
9 changes: 9 additions & 0 deletions mobile/lib/common/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get_10101/common/global_keys.dart';
import 'package:get_10101/common/settings/channel_screen.dart';
import 'package:get_10101/common/settings/emergency_kit_screen.dart';
import 'package:get_10101/common/settings/user_screen.dart';
import 'package:get_10101/common/settings/wallet_settings.dart';
import 'package:get_10101/common/status_screen.dart';
import 'package:get_10101/features/wallet/domain/destination.dart';
Expand Down Expand Up @@ -110,6 +111,14 @@ GoRouter createRoutes() {
return const WalletSettings();
},
),
GoRoute(
path: UserSettings.subRouteName,
// Use root navigator so the screen overlays the application shell
parentNavigatorKey: rootNavigatorKey,
builder: (BuildContext context, GoRouterState state) {
return const UserSettings();
},
),
GoRoute(
path: ChannelScreen.subRouteName,
// Use root navigator so the screen overlays the application shell
Expand Down
1 change: 0 additions & 1 deletion mobile/lib/common/service_status_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ class ServiceStatusNotifier extends ChangeNotifier implements Subscriber {
@override
void notify(bridge.Event event) {
if (event is bridge.Event_ServiceHealthUpdate) {
logger.t("Received event: ${event.toString()}");
var update = event.field0;
services[update.service] = update.status;

Expand Down
12 changes: 11 additions & 1 deletion mobile/lib/common/settings/settings_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:get_10101/common/settings/emergency_kit_screen.dart';
import 'package:get_10101/common/settings/force_close_screen.dart';
import 'package:get_10101/common/settings/open_telegram.dart';
import 'package:get_10101/common/settings/share_logs_screen.dart';
import 'package:get_10101/common/settings/user_screen.dart';
import 'package:get_10101/common/settings/wallet_settings.dart';
import 'package:get_10101/common/snack_bar.dart';
import 'package:get_10101/common/status_screen.dart';
Expand Down Expand Up @@ -176,7 +177,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
icon: Icons.wallet_outlined,
title: "Wallet Settings",
callBackFunc: () =>
GoRouter.of(context).push(WalletSettings.route))
GoRouter.of(context).push(WalletSettings.route)),
const Divider(
height: 0.5,
thickness: 0.8,
indent: 55,
),
SettingsClickable(
icon: FontAwesomeIcons.userAstronaut,
title: "User Settings",
callBackFunc: () => GoRouter.of(context).push(UserSettings.route))
],
),
)
Expand Down
Loading
Loading