supabase_auth_service.dart 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  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. // 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
  102. if (_auth.currentUser != null) {
  103. await _auth.signOut();
  104. }
  105. final provider = platform.toProvider();
  106. final completer = supabaseLoginCompleter(
  107. onSuccess: (userId, userEmail) async {
  108. return await setupAuth(
  109. map: {
  110. AuthServiceMapKeys.uuid: userId,
  111. AuthServiceMapKeys.email: userEmail,
  112. AuthServiceMapKeys.deviceId: await getDeviceId()
  113. },
  114. );
  115. },
  116. );
  117. final response = await _auth.signInWithOAuth(
  118. provider,
  119. queryParams: queryParamsForProvider(provider),
  120. redirectTo: supabaseLoginCallback,
  121. );
  122. if (!response) {
  123. completer.complete(left(AuthError.supabaseSignInWithOauthError));
  124. }
  125. return completer.future;
  126. }
  127. @override
  128. Future<void> signOut({
  129. AuthTypePB authType = AuthTypePB.Supabase,
  130. }) async {
  131. if (isSupabaseEnabled) {
  132. await _auth.signOut();
  133. }
  134. await _appFlowyAuthService.signOut(
  135. authType: authType,
  136. );
  137. }
  138. @override
  139. Future<Either<FlowyError, UserProfilePB>> signUpAsGuest({
  140. AuthTypePB authType = AuthTypePB.Supabase,
  141. Map<String, String> map = const {},
  142. }) async {
  143. // supabase don't support guest login.
  144. // so, just forward to our backend.
  145. return _appFlowyAuthService.signUpAsGuest();
  146. }
  147. @override
  148. Future<Either<FlowyError, UserProfilePB>> signInWithMagicLink({
  149. required String email,
  150. Map<String, String> map = const {},
  151. }) async {
  152. final completer = supabaseLoginCompleter(
  153. onSuccess: (userId, userEmail) async {
  154. return await setupAuth(
  155. map: {
  156. AuthServiceMapKeys.uuid: userId,
  157. AuthServiceMapKeys.email: userEmail,
  158. AuthServiceMapKeys.deviceId: await getDeviceId()
  159. },
  160. );
  161. },
  162. );
  163. await _auth.signInWithOtp(
  164. email: email,
  165. emailRedirectTo: kIsWeb ? null : supabaseLoginCallback,
  166. );
  167. return completer.future;
  168. }
  169. @override
  170. Future<Either<FlowyError, UserProfilePB>> getUser() async {
  171. return UserBackendService.getCurrentUserProfile();
  172. }
  173. Future<Either<FlowyError, User>> getSupabaseUser() async {
  174. final user = _auth.currentUser;
  175. if (user == null) {
  176. return left(AuthError.supabaseGetUserError);
  177. }
  178. return Right(user);
  179. }
  180. Future<Either<FlowyError, UserProfilePB>> setupAuth({
  181. required Map<String, String> map,
  182. }) async {
  183. final payload = ThirdPartyAuthPB(
  184. authType: AuthTypePB.Supabase,
  185. map: map,
  186. );
  187. return UserEventThirdPartyAuth(payload)
  188. .send()
  189. .then((value) => value.swap());
  190. }
  191. }
  192. extension on String {
  193. Provider toProvider() {
  194. switch (this) {
  195. case 'github':
  196. return Provider.github;
  197. case 'google':
  198. return Provider.google;
  199. case 'discord':
  200. return Provider.discord;
  201. default:
  202. throw UnimplementedError();
  203. }
  204. }
  205. }
  206. /// Creates a completer that listens to Supabase authentication state changes and
  207. /// completes when a user signs in.
  208. ///
  209. /// This function sets up a listener on Supabase's authentication state. When a user
  210. /// signs in, it triggers the provided [onSuccess] callback with the user's `id` and
  211. /// `email`. Once the [onSuccess] callback is executed and a response is received,
  212. /// the completer completes with the response, and the listener is canceled.
  213. ///
  214. /// Parameters:
  215. /// - [onSuccess]: A callback function that's executed when a user signs in. It
  216. /// should take in a user's `id` and `email` and return a `Future` containing either
  217. /// a `FlowyError` or a `UserProfilePB`.
  218. ///
  219. /// Returns:
  220. /// A completer of type `Either<FlowyError, UserProfilePB>`. This completer completes
  221. /// with the response from the [onSuccess] callback when a user signs in.
  222. Completer<Either<FlowyError, UserProfilePB>> supabaseLoginCompleter({
  223. required Future<Either<FlowyError, UserProfilePB>> Function(
  224. String userId,
  225. String userEmail,
  226. ) onSuccess,
  227. }) {
  228. final completer = Completer<Either<FlowyError, UserProfilePB>>();
  229. late final StreamSubscription<AuthState> subscription;
  230. final auth = Supabase.instance.client.auth;
  231. subscription = auth.onAuthStateChange.listen((event) async {
  232. final user = event.session?.user;
  233. if (event.event == AuthChangeEvent.signedIn && user != null) {
  234. final response = await onSuccess(
  235. user.id,
  236. user.email ?? user.newEmail ?? '',
  237. );
  238. // Only cancel the subscription if the Event is signedIn.
  239. subscription.cancel();
  240. completer.complete(response);
  241. }
  242. });
  243. return completer;
  244. }
  245. Map<String, String> queryParamsForProvider(Provider provider) {
  246. switch (provider) {
  247. case Provider.github:
  248. return {};
  249. case Provider.google:
  250. return {
  251. 'access_type': 'offline',
  252. 'prompt': 'consent',
  253. };
  254. case Provider.discord:
  255. return {};
  256. default:
  257. return {};
  258. }
  259. }