supabase_auth_service.dart 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import 'dart:async';
  2. import 'package:appflowy/startup/tasks/prelude.dart';
  3. import 'package:appflowy/user/application/auth/backend_auth_service.dart';
  4. import 'package:appflowy/user/application/auth/auth_service.dart';
  5. import 'package:appflowy/user/application/auth/device_id.dart';
  6. import 'package:appflowy/user/application/user_service.dart';
  7. import 'package:appflowy_backend/dispatch/dispatch.dart';
  8. import 'package:appflowy_backend/log.dart';
  9. import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
  10. import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
  11. import 'package:dartz/dartz.dart';
  12. import 'package:flutter/foundation.dart';
  13. import 'package:supabase_flutter/supabase_flutter.dart';
  14. import 'auth_error.dart';
  15. class SupabaseAuthService implements AuthService {
  16. SupabaseAuthService();
  17. SupabaseClient get _client => Supabase.instance.client;
  18. GoTrueClient get _auth => _client.auth;
  19. final BackendAuthService _backendAuthService = BackendAuthService(
  20. AuthTypePB.Supabase,
  21. );
  22. @override
  23. Future<Either<FlowyError, UserProfilePB>> signUp({
  24. required String name,
  25. required String email,
  26. required String password,
  27. Map<String, String> params = const {},
  28. }) async {
  29. // fetch the uuid from supabase.
  30. final response = await _auth.signUp(
  31. email: email,
  32. password: password,
  33. );
  34. final uuid = response.user?.id;
  35. if (uuid == null) {
  36. return left(AuthError.supabaseSignUpError);
  37. }
  38. // assign the uuid to our backend service.
  39. // and will transfer this logic to backend later.
  40. return _backendAuthService.signUp(
  41. name: name,
  42. email: email,
  43. password: password,
  44. params: {
  45. AuthServiceMapKeys.uuid: uuid,
  46. },
  47. );
  48. }
  49. @override
  50. Future<Either<FlowyError, UserProfilePB>> signIn({
  51. required String email,
  52. required String password,
  53. Map<String, String> params = const {},
  54. }) async {
  55. try {
  56. final response = await _auth.signInWithPassword(
  57. email: email,
  58. password: password,
  59. );
  60. final uuid = response.user?.id;
  61. if (uuid == null) {
  62. return Left(AuthError.supabaseSignInError);
  63. }
  64. return _backendAuthService.signIn(
  65. email: email,
  66. password: password,
  67. params: {
  68. AuthServiceMapKeys.uuid: uuid,
  69. },
  70. );
  71. } on AuthException catch (e) {
  72. Log.error(e);
  73. return Left(AuthError.supabaseSignInError);
  74. }
  75. }
  76. @override
  77. Future<Either<FlowyError, UserProfilePB>> signUpWithOAuth({
  78. required String platform,
  79. Map<String, String> params = const {},
  80. }) async {
  81. // Before signing in, sign out any existing users. Otherwise, the callback will be triggered even if the user doesn't click the 'Sign In' button on the website
  82. if (_auth.currentUser != null) {
  83. await _auth.signOut();
  84. }
  85. final provider = platform.toProvider();
  86. final completer = supabaseLoginCompleter(
  87. onSuccess: (userId, userEmail) async {
  88. return await _setupAuth(
  89. map: {
  90. AuthServiceMapKeys.uuid: userId,
  91. AuthServiceMapKeys.email: userEmail,
  92. AuthServiceMapKeys.deviceId: await getDeviceId()
  93. },
  94. );
  95. },
  96. );
  97. final response = await _auth.signInWithOAuth(
  98. provider,
  99. queryParams: queryParamsForProvider(provider),
  100. redirectTo: supabaseLoginCallback,
  101. );
  102. if (!response) {
  103. completer.complete(left(AuthError.supabaseSignInWithOauthError));
  104. }
  105. return completer.future;
  106. }
  107. @override
  108. Future<void> signOut() async {
  109. await _auth.signOut();
  110. await _backendAuthService.signOut();
  111. }
  112. @override
  113. Future<Either<FlowyError, UserProfilePB>> signUpAsGuest({
  114. Map<String, String> params = const {},
  115. }) async {
  116. // supabase don't support guest login.
  117. // so, just forward to our backend.
  118. return _backendAuthService.signUpAsGuest();
  119. }
  120. @override
  121. Future<Either<FlowyError, UserProfilePB>> signInWithMagicLink({
  122. required String email,
  123. Map<String, String> params = const {},
  124. }) async {
  125. final completer = supabaseLoginCompleter(
  126. onSuccess: (userId, userEmail) async {
  127. return await _setupAuth(
  128. map: {
  129. AuthServiceMapKeys.uuid: userId,
  130. AuthServiceMapKeys.email: userEmail,
  131. AuthServiceMapKeys.deviceId: await getDeviceId()
  132. },
  133. );
  134. },
  135. );
  136. await _auth.signInWithOtp(
  137. email: email,
  138. emailRedirectTo: kIsWeb ? null : supabaseLoginCallback,
  139. );
  140. return completer.future;
  141. }
  142. @override
  143. Future<Either<FlowyError, UserProfilePB>> getUser() async {
  144. return UserBackendService.getCurrentUserProfile();
  145. }
  146. Future<Either<FlowyError, User>> getSupabaseUser() async {
  147. final user = _auth.currentUser;
  148. if (user == null) {
  149. return left(AuthError.supabaseGetUserError);
  150. }
  151. return Right(user);
  152. }
  153. Future<Either<FlowyError, UserProfilePB>> _setupAuth({
  154. required Map<String, String> map,
  155. }) async {
  156. final payload = OauthSignInPB(
  157. authType: AuthTypePB.Supabase,
  158. map: map,
  159. );
  160. return UserEventOauthSignIn(payload).send().then((value) => value.swap());
  161. }
  162. }
  163. extension on String {
  164. Provider toProvider() {
  165. switch (this) {
  166. case 'github':
  167. return Provider.github;
  168. case 'google':
  169. return Provider.google;
  170. case 'discord':
  171. return Provider.discord;
  172. default:
  173. throw UnimplementedError();
  174. }
  175. }
  176. }
  177. /// Creates a completer that listens to Supabase authentication state changes and
  178. /// completes when a user signs in.
  179. ///
  180. /// This function sets up a listener on Supabase's authentication state. When a user
  181. /// signs in, it triggers the provided [onSuccess] callback with the user's `id` and
  182. /// `email`. Once the [onSuccess] callback is executed and a response is received,
  183. /// the completer completes with the response, and the listener is canceled.
  184. ///
  185. /// Parameters:
  186. /// - [onSuccess]: A callback function that's executed when a user signs in. It
  187. /// should take in a user's `id` and `email` and return a `Future` containing either
  188. /// a `FlowyError` or a `UserProfilePB`.
  189. ///
  190. /// Returns:
  191. /// A completer of type `Either<FlowyError, UserProfilePB>`. This completer completes
  192. /// with the response from the [onSuccess] callback when a user signs in.
  193. Completer<Either<FlowyError, UserProfilePB>> supabaseLoginCompleter({
  194. required Future<Either<FlowyError, UserProfilePB>> Function(
  195. String userId,
  196. String userEmail,
  197. ) onSuccess,
  198. }) {
  199. final completer = Completer<Either<FlowyError, UserProfilePB>>();
  200. late final StreamSubscription<AuthState> subscription;
  201. final auth = Supabase.instance.client.auth;
  202. subscription = auth.onAuthStateChange.listen((event) async {
  203. final user = event.session?.user;
  204. if (event.event == AuthChangeEvent.signedIn && user != null) {
  205. final response = await onSuccess(
  206. user.id,
  207. user.email ?? user.newEmail ?? '',
  208. );
  209. // Only cancel the subscription if the Event is signedIn.
  210. subscription.cancel();
  211. completer.complete(response);
  212. }
  213. });
  214. return completer;
  215. }
  216. Map<String, String> queryParamsForProvider(Provider provider) {
  217. switch (provider) {
  218. case Provider.github:
  219. return {};
  220. case Provider.google:
  221. return {
  222. 'access_type': 'offline',
  223. 'prompt': 'consent',
  224. };
  225. case Provider.discord:
  226. return {};
  227. default:
  228. return {};
  229. }
  230. }