Photo by Papaioannou Kostas on Unsplash

Flashlist PART 2: Let there be users

Empowering User Interactions: Leveraging Serverpod and Riverpod Providers

Ben Auer
3 min readMar 18, 2024

--

Intro

In my previous post, I discussed setting up ServerRiverPodPod and how to gracefully handle authentication state changes. However authentication is just the beginning when it comes to app interactions. After a user confirms their identity, we need an object to represent them that holds no sensitive information.

In case you missed the first part or want to know why I’m building Flashlist, you can follow the links below:

  1. Flashlists Journey with Cutting Edge Technology
  2. Flashlist PART 1: Serverpod + Riverpod = ServerRiverPodPod

AppUser

Within the models directory, we add an ‘AppUser’ class that represents the user within the application.

class: AppUser
table: flashlist_app_user
fields:
userId: int, relation(parent=serverpod_user_info, onUpdate=Cascade, onDelete=Cascade)
username: String
email: String
imageSrc: String?

After running serverpod generate we then add the onUserCreated method to the auth config.

// ...server/lib/server.dart
auth.AuthConfig.set(
auth.AuthConfig(
sendValidationEmail: (session, email, validationCode) async {
// Handle code sending validation email
return true;
},
sendPasswordResetEmail: (session, userInfo, validationCode) async {
// Send the password reset email to the user.
return true;
},
onUserCreated: (session, userInfo) {
// Create a AppUser object with the information of the created auth-user
return AppUser.db.insertRow(
session,
AppUser(
userId: userInfo.id!,
email: userInfo.email!,
username: userInfo.userName,
),
);
},
),
);

We also need 3 other models: Notification, UserRequest and UserRelation. You can look at them here, but in case you couldn’t be bothered I’ll brush over them quickly:

Notification — A notification about an event that the user should be informed about.

UserRequest — A request from one user to another, such as a friend request or invitation.

UserRelation — A connection between two users.

Endpoint

Next, we declare the AppUserEndpoint, from which we plan on dealing with user-specific logic.

// ...server/lib/src/endpoints/app_user_endpoint.dart
class AppUserEndpoint extends Endpoint {
Future<AppUser?> getCurrentUser(Session session) async {
final currentUserId = await session.auth.authenticatedUserId;


return await AppUser.db.findFirstRow(
session,
where: (appUser) => appUser.userId.equals(currentUserId),
);
}


Future<AppUser?> getUserByEmail(Session session, String email) async {
return await AppUser.db.findFirstRow(
session,
where: (appUser) => appUser.email.equals(email),
);
}
}

Note: The Endpoint quickly got very large, so I won’t be sharing it in full here, but you can take a look at it here.

UserController

Our UserController serves as a Single Source of Truth (SOT) for all methods exposed by the AppUserEndpoint to the frontend.

// ...flutter/lib/src/features/users/application/user_controller.dart

part 'user_controller.g.dart';


class UserController {
/// Controller for user related actions.
UserController(this.ref);


final Ref ref;


Future<AppUser?> getCurrentUser() async {
final client = ref.read(clientProvider);
return await client.appUser.getCurrentUser();
}


Future<AppUser?> getUserById(int id) async {
final client = ref.read(clientProvider);
return await client.appUser.getUserById(id);
}


Future<void> sendConnectionRequestByEmail(String email) async {
final client = ref.read(clientProvider);
await client.appUser.sendConnectionRequestByEmail(email);
}


Future<void> acceptConnectionRequest(int requestId) async {
final client = ref.read(clientProvider);
await client.appUser.acceptConnectionRequest(requestId);
}


Future<void> removeRequest(int requestId) async {
final client = ref.read(clientProvider);
await client.appUser.removeRequest(requestId);
}


Future<List<AppUser>> getConnections() async {
final client = ref.read(clientProvider);
return await client.appUser.getConnections();
}


Future<List<UserRequest?>> getPendingRequests() async {
final client = ref.read(clientProvider);
return await client.appUser.getRequestsForUser();
}
}


@riverpod
UserController userController(Ref ref) => UserController(ref);


@riverpod
Future<AppUser?> currentUser(CurrentUserRef ref) =>
ref.watch(userControllerProvider).getCurrentUser();


@riverpod
Future<List<AppUser?>> connections(ConnectionsRef ref) =>
ref.watch(userControllerProvider).getConnections();


@riverpod
Future<List<UserRequest?>> pendingRequests(PendingRequestsRef ref) =>
ref.watch(userControllerProvider).getPendingRequests();


@riverpod
Future<AppUser?> userById(UserByIdRef ref, int id) =>
ref.watch(userControllerProvider).getUserById(id);

By following this pattern we’ve established a solid foundation for managing users, user requests, user relations, and notifications within the app.

Conclusion

Properly managing user data is fundamental for any developer. By separating the authentication user from the app user, we can safeguard sensitive data, ensuring it’s only exposed when a user authenticates or engages with the platform.

Clap if you agree! 👏👏👏👏

Next: Flashlist PART 3: Implementing the CRUD Stream

--

--