supabase_auth_service.dart 7.6 KB

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