supabase_auth_service.dart 7.9 KB

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