supabase_auth_service.dart 7.7 KB

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