Skip to content

Commit

Permalink
feat: allow user to change their contact details
Browse files Browse the repository at this point in the history
Also, users can register with Nostr, email or telegram. Note there is no input validation so the user can put anything :)
  • Loading branch information
bonomat committed Feb 9, 2024
1 parent da0c291 commit 1db3ebb
Show file tree
Hide file tree
Showing 17 changed files with 344 additions and 63 deletions.
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))
.route("/api/users/:trader_pubkey", get(get_user))
.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
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

0 comments on commit 1db3ebb

Please sign in to comment.