feat: custom server url in application (#3996)

* chore:test

* chore: update ui

* feat: set appflowy cloud url

* chore: add self host docs

* fix: save user

* fix: sign out when authenticator not match

* fix: sign out when authenticator not match

* fix: db lock

* chore: remove unuse env file

* test: disable supabase cloud test

* test: disable supabase cloud test

* chore: fix save
This commit is contained in:
Nathan.fooo
2023-11-24 11:54:47 +08:00
committed by GitHub
parent e18e031710
commit 1fad713477
63 changed files with 1758 additions and 721 deletions

View File

@ -49,6 +49,10 @@ class SupabaseConfiguration {
anon_key: '',
);
}
bool get isValid {
return url.isNotEmpty && anon_key.isNotEmpty;
}
}
@JsonSerializable()
@ -75,4 +79,10 @@ class AppFlowyCloudConfiguration {
gotrue_url: '',
);
}
bool get isValid {
return base_url.isNotEmpty &&
ws_base_url.isNotEmpty &&
gotrue_url.isNotEmpty;
}
}

View File

@ -1,76 +1,104 @@
// lib/env/env.dart
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/env/backend_env.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/log.dart';
import 'package:envied/envied.dart';
import 'package:dartz/dartz.dart';
part 'env.g.dart';
/// The environment variables are defined in `.env` file that is located in the
/// appflowy_flutter.
/// Run `dart run build_runner build --delete-conflicting-outputs`
/// to generate the keys from the env file.
/// Sets the cloud type for the application.
///
/// If you want to regenerate the keys, you need to run `dart run
/// build_runner clean` before running `dart run build_runner build
/// --delete-conflicting-outputs`.
/// Follow the guide on https://supabase.com/docs/guides/auth/social-login/auth-google to setup the auth provider.
/// This method updates the cloud type setting in the key-value storage
/// using the [KeyValueStorage] service. The cloud type is identified
/// by the [CloudType] enum.
///
@Envied(path: '.env')
abstract class Env {
@EnviedField(
obfuscate: true,
varName: 'CLOUD_TYPE',
defaultValue: '0',
)
static final int cloudType = _Env.cloudType;
/// AppFlowy Cloud Configuration
@EnviedField(
obfuscate: true,
varName: 'APPFLOWY_CLOUD_BASE_URL',
defaultValue: '',
)
static final String afCloudBaseUrl = _Env.afCloudBaseUrl;
@EnviedField(
obfuscate: true,
varName: 'APPFLOWY_CLOUD_WS_BASE_URL',
defaultValue: '',
)
static final String afCloudWSBaseUrl = _Env.afCloudWSBaseUrl;
@EnviedField(
obfuscate: true,
varName: 'APPFLOWY_CLOUD_GOTRUE_URL',
defaultValue: '',
)
static final String afCloudGoTrueUrl = _Env.afCloudGoTrueUrl;
// Supabase Configuration:
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_URL',
defaultValue: '',
)
static final String supabaseUrl = _Env.supabaseUrl;
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_ANON_KEY',
defaultValue: '',
)
static final String supabaseAnonKey = _Env.supabaseAnonKey;
/// [ty] - The type of cloud to be set. It must be one of the values from
/// [CloudType] enum. The corresponding integer value of the enum is stored:
/// - `CloudType.local` is stored as "0".
/// - `CloudType.supabase` is stored as "1".
/// - `CloudType.appflowyCloud` is stored as "2".
Future<void> setCloudType(CloudType ty) async {
switch (ty) {
case CloudType.local:
getIt<KeyValueStorage>().set(KVKeys.kCloudType, 0.toString());
break;
case CloudType.supabase:
getIt<KeyValueStorage>().set(KVKeys.kCloudType, 1.toString());
break;
case CloudType.appflowyCloud:
getIt<KeyValueStorage>().set(KVKeys.kCloudType, 2.toString());
break;
}
}
bool get isCloudEnabled {
/// Retrieves the currently set cloud type.
///
/// This method fetches the cloud type setting from the key-value storage
/// using the [KeyValueStorage] service and returns the corresponding
/// [CloudType] enum value.
///
/// Returns:
/// A Future that resolves to a [CloudType] enum value representing the
/// currently set cloud type. The default return value is `CloudType.local`
/// if no valid setting is found.
///
Future<CloudType> getCloudType() async {
final value = await getIt<KeyValueStorage>().get(KVKeys.kCloudType);
return value.fold(() => CloudType.local, (s) {
switch (s) {
case "0":
return CloudType.local;
case "1":
return CloudType.supabase;
case "2":
return CloudType.appflowyCloud;
default:
return CloudType.local;
}
});
}
/// Determines whether authentication is enabled.
///
/// This getter evaluates if authentication should be enabled based on the
/// current integration mode and cloud type settings.
///
/// Returns:
/// A boolean value indicating whether authentication is enabled. It returns
/// `true` if the application is in release or develop mode, and the cloud type
/// is not set to `CloudType.local`. Additionally, it checks if either the
/// AppFlowy Cloud or Supabase configuration is valid.
/// Returns `false` otherwise.
bool get isAuthEnabled {
// Only enable supabase in release and develop mode.
if (integrationMode().isRelease || integrationMode().isDevelop) {
return currentCloudType().isEnabled;
final env = getIt<AppFlowyCloudSharedEnv>();
if (env.cloudType == CloudType.local) {
return false;
}
if (env.cloudType == CloudType.supabase) {
return env.supabaseConfig.isValid;
}
if (env.cloudType == CloudType.appflowyCloud) {
return env.appflowyCloudConfig.isValid;
}
return false;
} else {
return false;
}
}
/// Checks if Supabase is enabled.
///
/// This getter evaluates if Supabase should be enabled based on the
/// current integration mode and cloud type setting.
///
/// Returns:
/// A boolean value indicating whether Supabase is enabled. It returns `true`
/// if the application is in release or develop mode and the current cloud type
/// is `CloudType.supabase`. Otherwise, it returns `false`.
bool get isSupabaseEnabled {
// Only enable supabase in release and develop mode.
if (integrationMode().isRelease || integrationMode().isDevelop) {
@ -80,6 +108,15 @@ bool get isSupabaseEnabled {
}
}
/// Determines if AppFlowy Cloud is enabled.
///
/// This getter assesses if AppFlowy Cloud should be enabled based on the
/// current integration mode and cloud type setting.
///
/// Returns:
/// A boolean value indicating whether AppFlowy Cloud is enabled. It returns
/// `true` if the application is in release or develop mode and the current
/// cloud type is `CloudType.appflowyCloud`. Otherwise, it returns `false`.
bool get isAppFlowyCloudEnabled {
// Only enable appflowy cloud in release and develop mode.
if (integrationMode().isRelease || integrationMode().isDevelop) {
@ -90,40 +127,125 @@ bool get isAppFlowyCloudEnabled {
}
enum CloudType {
unknown,
local,
supabase,
appflowyCloud;
bool get isEnabled => this != CloudType.unknown;
bool get isEnabled => this != CloudType.local;
int get value {
switch (this) {
case CloudType.local:
return 0;
case CloudType.supabase:
return 1;
case CloudType.appflowyCloud:
return 2;
}
}
}
CloudType currentCloudType() {
final value = Env.cloudType;
if (value == 1) {
if (Env.supabaseUrl.isEmpty || Env.supabaseAnonKey.isEmpty) {
Log.error(
"Supabase is not configured correctly. The values are: "
"url: ${Env.supabaseUrl}, anonKey: ${Env.supabaseAnonKey}",
);
return CloudType.unknown;
} else {
return CloudType.supabase;
}
}
if (value == 2) {
if (Env.afCloudBaseUrl.isEmpty ||
Env.afCloudWSBaseUrl.isEmpty ||
Env.afCloudGoTrueUrl.isEmpty) {
Log.error(
"AppFlowy cloud is not configured correctly. The values are: "
"baseUrl: ${Env.afCloudBaseUrl}, wsBaseUrl: ${Env.afCloudWSBaseUrl}, gotrueUrl: ${Env.afCloudGoTrueUrl}",
);
return CloudType.unknown;
} else {
return CloudType.appflowyCloud;
}
}
return CloudType.unknown;
return getIt<AppFlowyCloudSharedEnv>().cloudType;
}
Future<void> setAppFlowyCloudBaseUrl(Option<String> url) async {
await url.fold(
() => getIt<KeyValueStorage>().remove(KVKeys.kAppflowyCloudBaseURL),
(s) => getIt<KeyValueStorage>().set(KVKeys.kAppflowyCloudBaseURL, s),
);
}
/// Use getIt<AppFlowyCloudSharedEnv>() to get the shared environment.
class AppFlowyCloudSharedEnv {
final CloudType cloudType;
final AppFlowyCloudConfiguration appflowyCloudConfig;
final SupabaseConfiguration supabaseConfig;
AppFlowyCloudSharedEnv({
required this.cloudType,
required this.appflowyCloudConfig,
required this.supabaseConfig,
});
}
Future<AppFlowyCloudConfiguration> getAppFlowyCloudConfig() async {
return AppFlowyCloudConfiguration(
base_url: await getAppFlowyCloudUrl(),
ws_base_url: await _getAppFlowyCloudWSUrl(),
gotrue_url: await _getAppFlowyCloudGotrueUrl(),
);
}
Future<String> getAppFlowyCloudUrl() async {
final result =
await getIt<KeyValueStorage>().get(KVKeys.kAppflowyCloudBaseURL);
return result.fold(
() => "",
(url) => url,
);
}
Future<String> _getAppFlowyCloudWSUrl() async {
try {
final serverUrl = await getAppFlowyCloudUrl();
final uri = Uri.parse(serverUrl);
// Construct the WebSocket URL directly from the parsed URI.
final wsScheme = uri.isScheme('HTTPS') ? 'wss' : 'ws';
final wsUrl = Uri(scheme: wsScheme, host: uri.host, path: '/ws');
return wsUrl.toString();
} catch (e) {
Log.error("Failed to get WebSocket URL: $e");
return "";
}
}
Future<String> _getAppFlowyCloudGotrueUrl() async {
final serverUrl = await getAppFlowyCloudUrl();
return "$serverUrl/gotrue";
}
Future<void> setSupbaseServer(
Option<String> url,
Option<String> anonKey,
) async {
assert(
(url.isSome() && anonKey.isSome()) || (url.isNone() && anonKey.isNone()),
"Either both Supabase URL and anon key must be set, or both should be unset",
);
await url.fold(
() => getIt<KeyValueStorage>().remove(KVKeys.kSupabaseURL),
(s) => getIt<KeyValueStorage>().set(KVKeys.kSupabaseURL, s),
);
await anonKey.fold(
() => getIt<KeyValueStorage>().remove(KVKeys.kSupabaseAnonKey),
(s) => getIt<KeyValueStorage>().set(KVKeys.kSupabaseAnonKey, s),
);
}
Future<SupabaseConfiguration> getSupabaseCloudConfig() async {
final url = await _getSupbaseUrl();
final anonKey = await _getSupabaseAnonKey();
return SupabaseConfiguration(
url: url,
anon_key: anonKey,
);
}
Future<String> _getSupbaseUrl() async {
final result = await getIt<KeyValueStorage>().get(KVKeys.kSupabaseURL);
return result.fold(
() => "",
(url) => url,
);
}
Future<String> _getSupabaseAnonKey() async {
final result = await getIt<KeyValueStorage>().get(KVKeys.kSupabaseAnonKey);
return result.fold(
() => "",
(url) => url,
);
}