misskey_auth 0.1.2-beta copy "misskey_auth: ^0.1.2-beta" to clipboard
misskey_auth: ^0.1.2-beta copied to clipboard

Flutter library for Misskey OAuth authentication. Support for MiAuth format is planned in the future.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:misskey_auth/misskey_auth.dart';
import 'package:loader_overlay/loader_overlay.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Misskey Auth Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: LoaderOverlay(
        child: const AuthExamplePage(),
      ),
    );
  }
}

class AuthExamplePage extends StatefulWidget {
  const AuthExamplePage({super.key});

  @override
  State<AuthExamplePage> createState() => _AuthExamplePageState();
}

class _AuthExamplePageState extends State<AuthExamplePage> {
  final _client = MisskeyOAuthClient();
  final _miClient = MisskeyMiAuthClient();
  int _currentIndex = 0;

  // フォームコントローラー
  final _hostController = TextEditingController();
  final _clientIdController = TextEditingController();
  final _redirectUriController = TextEditingController();
  final _scopeController = TextEditingController();
  final _callbackSchemeController = TextEditingController();

  // MiAuth 用フォーム
  final _miAppNameController = TextEditingController();
  final _miPermissionsController = TextEditingController();
  final _miIconUrlController = TextEditingController();

  // 状態
  String? _accessToken;
  OAuthServerInfo? _serverInfo;

  String _mapErrorToMessage(Object error) {
    // MisskeyAuth のカスタム例外をユーザー向け日本語に整形
    if (error is MisskeyAuthException) {
      final details = error.details;
      if (error is UserCancelledException) {
        return '認証がキャンセルされました';
      }
      if (error is CallbackSchemeErrorException) {
        return 'コールバックスキームの設定が正しくありません(AndroidManifest/Info.plist を確認してください)';
      }
      if (error is AuthorizationLaunchException) {
        return '認証画面を起動できませんでした';
      }
      if (error is NetworkException) {
        return 'ネットワークエラーが発生しました';
      }
      if (error is ResponseParseException) {
        return 'サーバー応答の解析に失敗しました';
      }
      if (error is SecureStorageException) {
        return 'セキュアストレージの操作に失敗しました';
      }
      if (error is InvalidAuthConfigException) {
        return '認証設定が無効です';
      }
      if (error is ServerInfoException) {
        return 'サーバー情報の取得に失敗しました${details != null ? ': $details' : ''}';
      }
      // OAuth
      if (error is OAuthNotSupportedException) {
        return 'このサーバーはOAuth認証に対応していません(MiAuthをご利用ください)';
      }
      if (error is StateMismatchException) {
        return 'セキュリティ検証に失敗しました(state不一致)';
      }
      if (error is AuthorizationCodeMissingException) {
        return '認証コードを取得できませんでした';
      }
      if (error is AuthorizationServerErrorException) {
        return '認可サーバーでエラーが発生しました${details != null ? ': $details' : ''}';
      }
      if (error is TokenExchangeException) {
        return 'トークン交換に失敗しました${details != null ? ': $details' : ''}';
      }
      // MiAuth
      if (error is MiAuthDeniedException) {
        return 'MiAuth がキャンセル/拒否されました';
      }
      if (error is MiAuthCheckFailedException) {
        return 'MiAuth の検証に失敗しました${details != null ? ': $details' : ''}';
      }
      if (error is MiAuthSessionInvalidException) {
        return 'MiAuth のセッションが無効または期限切れです${details != null ? ': $details' : ''}';
      }
      return error.toString();
    }
    // その他の例外はそのまま文字列化
    return error.toString();
  }

  @override
  void initState() {
    super.initState();
    _setDefaultValues();
    _loadStoredToken();
  }

  @override
  void dispose() {
    _hostController.dispose();
    _clientIdController.dispose();
    _redirectUriController.dispose();
    _scopeController.dispose();
    _callbackSchemeController.dispose();
    _miAppNameController.dispose();
    _miPermissionsController.dispose();
    _miIconUrlController.dispose();
    super.dispose();
  }

  void _setDefaultValues() {
    _hostController.text = 'misskey.io';
    _clientIdController.text =
        'https://librarylibrarian.github.io/misskey_auth/';
    _redirectUriController.text =
        'https://librarylibrarian.github.io/misskey_auth/redirect.html';
    _scopeController.text = 'read:account write:notes';
    _callbackSchemeController.text = 'misskeyauth';

    // MiAuth
    _miAppNameController.text = 'Misskey Auth Example';
    _miPermissionsController.text = 'read:account write:notes';
    _miIconUrlController.text = '';
  }

  Future<void> _loadStoredToken() async {
    try {
      final token = await _client.getStoredAccessToken();
      setState(() {
        _accessToken = token;
      });
    } on MisskeyAuthException catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(_mapErrorToMessage(e))),
        );
      }
    } catch (_) {}
  }

  Future<void> _checkServerInfo() async {
    setState(() {
      _serverInfo = null;
    });

    if (!mounted) return;
    context.loaderOverlay.show();

    try {
      final host = _hostController.text.trim();
      if (host.isEmpty) {
        throw Exception('ホストを入力してください');
      }

      final serverInfo = await _client.getOAuthServerInfo(host);

      if (!mounted) return;
      setState(() {
        _serverInfo = serverInfo;
      });

      if (serverInfo == null && mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
              content: Text('OAuth認証はサポートされていません(MiAuth認証を使用してください)')),
        );
      }
    } on MisskeyAuthException catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(_mapErrorToMessage(e))),
        );
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(e.toString())),
        );
      }
    } finally {
      if (mounted) {
        context.loaderOverlay.hide();
      }
    }
  }

  Future<void> _startAuth() async {
    if (!mounted) return;
    context.loaderOverlay.show();

    try {
      final config = MisskeyOAuthConfig(
        host: _hostController.text.trim(),
        clientId: _clientIdController.text.trim(),
        redirectUri: _redirectUriController.text.trim(),
        scope: _scopeController.text.trim(),
        callbackScheme: _callbackSchemeController.text.trim(),
      );

      final tokenResponse = await _client.authenticate(config);

      if (tokenResponse != null && mounted) {
        setState(() {
          _accessToken = tokenResponse.accessToken;
        });

        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('認証に成功しました!')),
          );
        }
      }
    } on MisskeyAuthException catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(_mapErrorToMessage(e))),
        );
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('認証エラー: $e')),
        );
      }
    } finally {
      if (mounted) {
        context.loaderOverlay.hide();
      }
    }
  }

  Future<void> _startMiAuth() async {
    if (!mounted) return;
    context.loaderOverlay.show();

    try {
      final host = _hostController.text.trim();
      if (host.isEmpty) {
        throw Exception('ホストを入力してください');
      }

      final scheme = _callbackSchemeController.text.trim();
      if (scheme.isEmpty) {
        throw Exception('コールバックスキームを入力してください');
      }

      final permissions = _miPermissionsController.text
          .split(RegExp(r"[ ,]+"))
          .where((e) => e.isNotEmpty)
          .toList();

      final config = MisskeyMiAuthConfig(
        host: host,
        appName: _miAppNameController.text.trim(),
        callbackScheme: scheme,
        permissions: permissions,
        iconUrl: _miIconUrlController.text.trim().isEmpty
            ? null
            : _miIconUrlController.text.trim(),
      );

      final res = await _miClient.authenticate(config);

      if (mounted) {
        setState(() {
          _accessToken = res.token;
        });

        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('MiAuth に成功しました!')),
          );
        }
      }
    } on MisskeyAuthException catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(_mapErrorToMessage(e))),
        );
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('MiAuth エラー: $e')),
        );
      }
    } finally {
      if (mounted) {
        context.loaderOverlay.hide();
      }
    }
  }

  Future<void> _clearToken() async {
    if (!mounted) return;
    context.loaderOverlay.show();

    try {
      await _client.clearTokens();
      await _loadStoredToken();

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('トークンを削除しました')),
        );
      }
    } on MisskeyAuthException catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(_mapErrorToMessage(e))),
        );
      }
    } finally {
      if (mounted) {
        context.loaderOverlay.hide();
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Misskey Auth Sample'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            _buildStoredTokenCard(),
            const SizedBox(height: 16),
            if (_currentIndex == 0)
              _buildOAuthForm(context)
            else if (_currentIndex == 1)
              _buildMiAuthForm(context)
            else
              _buildServerInfoTab(context),
            // 画面内のエラーカード表示は行わず、Snackbarのみで通知
          ],
        ),
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        destinations: const [
          NavigationDestination(icon: Icon(Icons.lock), label: 'OAuth'),
          NavigationDestination(icon: Icon(Icons.vpn_key), label: 'MiAuth'),
          NavigationDestination(
              icon: Icon(Icons.info_outline), label: 'サーバー情報'),
        ],
        onDestinationSelected: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
      ),
    );
  }

  Widget _buildStoredTokenCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              '保存されたトークン',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Text(_accessToken != null
                ? '${_accessToken!.substring(0, 10)}...'
                : 'トークンなし'),
            if (_accessToken != null) ...[
              const SizedBox(height: 8),
              ElevatedButton(
                onPressed: _clearToken,
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.red,
                  foregroundColor: Colors.white,
                ),
                child: const Text('トークンを削除'),
              ),
            ],
          ],
        ),
      ),
    );
  }

  Widget _buildOAuthForm(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'OAuth認証設定',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            TextField(
              controller: _callbackSchemeController,
              decoration: const InputDecoration(
                labelText: 'コールバックスキーム',
                hintText: '例: misskeyauth',
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _hostController,
              decoration: const InputDecoration(
                labelText: 'ホスト',
                hintText: '例: misskey.io',
              ),
            ),
            const SizedBox(height: 8),
            TextField(
              controller: _clientIdController,
              decoration: const InputDecoration(
                labelText: 'クライアントID (URL)',
                hintText: '例: https://example.com/my-app',
              ),
            ),
            const SizedBox(height: 8),
            TextField(
              controller: _redirectUriController,
              decoration: const InputDecoration(
                labelText: 'リダイレクトURI',
                hintText: '例: https://example.com/redirect',
                helperText: '要HTTPS',
              ),
            ),
            const SizedBox(height: 8),
            TextField(
              controller: _scopeController,
              decoration: const InputDecoration(
                labelText: 'スコープ',
                hintText: '例: read:account write:notes',
              ),
            ),
            const SizedBox(height: 16),
            Row(
              children: [
                ElevatedButton(
                  onPressed: _startAuth,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Theme.of(context).colorScheme.primary,
                    foregroundColor: Colors.white,
                  ),
                  child: const Text('認証を開始'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildMiAuthForm(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'MiAuth認証設定',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            TextField(
              controller: _callbackSchemeController,
              decoration: const InputDecoration(
                labelText: 'コールバックスキーム',
                hintText: '例: misskeyauth',
              ),
            ),
            const SizedBox(height: 8),
            TextField(
              controller: _hostController,
              decoration: const InputDecoration(
                labelText: 'ホスト',
                hintText: '例: misskey.io',
              ),
            ),
            const SizedBox(height: 8),
            TextField(
              controller: _miAppNameController,
              decoration: const InputDecoration(
                labelText: 'アプリ名',
                hintText: '例: Misskey Auth Example',
              ),
            ),
            const SizedBox(height: 8),
            TextField(
              controller: _miPermissionsController,
              decoration: const InputDecoration(
                labelText: '権限(空白/カンマ区切り)',
                hintText: '例: read:account write:notes',
              ),
            ),
            const SizedBox(height: 8),
            TextField(
              controller: _miIconUrlController,
              decoration: const InputDecoration(
                labelText: 'アイコンURL(任意)',
              ),
            ),
            const SizedBox(height: 16),
            Row(
              children: [
                ElevatedButton(
                  onPressed: _startMiAuth,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Theme.of(context).colorScheme.primary,
                    foregroundColor: Colors.white,
                  ),
                  child: const Text('MiAuthで認証'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildServerInfoCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'サーバー情報',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Text('認証エンドポイント:\n${_serverInfo!.authorizationEndpoint}'),
            const SizedBox(height: 4),
            Text('トークンエンドポイント:\n${_serverInfo!.tokenEndpoint}'),
            if (_serverInfo!.scopesSupported != null) ...[
              const SizedBox(height: 4),
              Text(
                  'サポートされているスコープ:\n${_serverInfo!.scopesSupported!.join(', ')}'),
            ],
          ],
        ),
      ),
    );
  }

  Widget _buildServerInfoTab(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Card(
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text(
                  'サーバー情報の確認',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 8),
                TextField(
                  controller: _hostController,
                  decoration: const InputDecoration(
                    labelText: 'ホスト',
                    hintText: '例: misskey.io',
                  ),
                ),
                const SizedBox(height: 16),
                ElevatedButton(
                  onPressed: _checkServerInfo,
                  child: const Text('サーバー情報を確認'),
                ),
              ],
            ),
          ),
        ),
        if (_serverInfo != null) ...[
          const SizedBox(height: 16),
          _buildServerInfoCard(),
        ],
      ],
    );
  }

  // 画面内のエラーカードは廃止(Snackbarのみ使用)
}
0
likes
0
points
6
downloads

Publisher

verified publisherlibrarylibrarian.com

Weekly Downloads

Flutter library for Misskey OAuth authentication. Support for MiAuth format is planned in the future.

Homepage
Repository (GitHub)
View/report issues

Topics

#authentication #oauth #misskey #social-media #api

License

unknown (license)

Dependencies

crypto, cupertino_icons, dio, flutter, flutter_secure_storage, flutter_web_auth_2

More

Packages that depend on misskey_auth