From fa106997be10c68bd05192af6151764ee840e412 Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Mon, 6 Jan 2025 12:44:32 +0000 Subject: [PATCH 01/19] Initial draft --- .../products/auth/team-invites/+page.markdoc | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 src/routes/docs/products/auth/team-invites/+page.markdoc diff --git a/src/routes/docs/products/auth/team-invites/+page.markdoc b/src/routes/docs/products/auth/team-invites/+page.markdoc new file mode 100644 index 0000000000..2f522d31ac --- /dev/null +++ b/src/routes/docs/products/auth/team-invites/+page.markdoc @@ -0,0 +1,245 @@ +--- +layout: article +title: Team Invites +description: Learn how to manage team invites in Appwrite. Explore both client-side and server-side approaches for inviting users to teams. +--- + +Teams in Appwrite allow users to share access to resources. When you invite users to a team, they can access resources based on their team roles and permissions. There are two main approaches to handling team invites: client-side (using email) and server-side (for custom flows). + +# Client-side invites {% #client-side %} + +Client-side invites are the simplest way to invite users to a team. When you create a membership, Appwrite automatically sends an email invitation to the user. + +{% multicode %} +```client-web +import { Client, Teams } from "appwrite"; + +const client = new Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); + +const teams = new Teams(client); + +// Invite a user by email +const membership = await teams.createMembership( + 'team_id', + ['developer'], // roles + 'user@example.com' // email +); +``` +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); + +final teams = Teams(client); + +// Invite a user by email +final membership = await teams.createMembership( + teamId: 'team_id', + roles: ['developer'], + email: 'user@example.com' +); +``` +{% /multicode %} + +The invited user will receive an email with a link to accept the invitation. When they click the link, they'll be redirected to your app where you can handle the invitation acceptance: + +{% multicode %} +```client-web +// Handle the invitation acceptance +const membership = await teams.updateMembershipStatus( + 'team_id', + 'membership_id', + 'user_id', + 'secret' +); +``` +```client-flutter +// Handle the invitation acceptance +final membership = await teams.updateMembershipStatus( + teamId: 'team_id', + membershipId: 'membership_id', + userId: 'user_id', + secret: 'secret' +); +``` +{% /multicode %} + +# Server-side invites {% #server-side %} + +Server-side invites give you more control over the invitation process. Instead of sending automatic emails, you can create custom flows using the server SDK. + +{% multicode %} +```server-nodejs +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const teams = new sdk.Teams(client); + +// Create membership directly with userId +const membership = await teams.createMembership( + teamId: 'team_id', + roles: ['developer'], + userId: 'user_id', + name: 'John Doe' // optional +); +``` +```python +from appwrite.client import Client +from appwrite.services.teams import Teams + +client = Client() +client.set_endpoint('https://cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +teams = Teams(client) + +# Create membership directly with userId +membership = teams.create_membership( + team_id='team_id', + roles=['developer'], + user_id='user_id', + name='John Doe' # optional +) +``` +{% /multicode %} + +Server-side invites are useful when: +- You want to implement your own invitation email system +- You're building a custom invitation workflow +- You want to automatically add users to teams based on certain conditions +- You need to handle bulk invites efficiently + +# Understanding roles {% #roles %} + +Team roles help you control what members can do within a team. When creating a membership, you can assign one or more roles: + +1. **owner** - Special role that can: + - Invite new members + - Remove members + - Update team settings + - Delete the team + +2. **Custom roles** - You define what these roles mean in your permissions: + - Can be any string (e.g., 'admin', 'developer', 'viewer') + - Used in combination with [permissions](/docs/advanced/platform/permissions) + - Multiple roles can be assigned to one member + +Example of using roles in permissions: + +```javascript +// Grant access to users with 'developer' role in team_123 +['team:team_123/developer'] + +// Grant access to all members of team_123 +['team:team_123'] +``` + +# Custom invitation flows {% #custom-flows %} + +Here's an example of implementing a custom invitation flow: + +1. Create a server-side function to handle invites: + +{% multicode %} +```server-nodejs +const sdk = require('node-appwrite'); + +async function inviteToTeam(teamId, userId, roles) { + const client = new sdk.Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + + const teams = new sdk.Teams(client); + + try { + // Create membership + const membership = await teams.createMembership( + teamId, + roles, + userId, + 'John Doe' // optional name + ); + + // Implement your custom notification logic here + // For example, send a custom email, push notification, etc. + + return membership; + } catch (error) { + console.error('Failed to invite user:', error); + throw error; + } +} +``` +```python +from appwrite.client import Client +from appwrite.services.teams import Teams + +async def invite_to_team(team_id, user_id, roles): + client = Client() + client.set_endpoint('https://cloud.appwrite.io/v1') + client.set_project('') + client.set_key('') + + teams = Teams(client) + + try: + # Create membership + membership = teams.create_membership( + team_id=team_id, + roles=roles, + user_id=user_id, + name='John Doe' # optional + ) + + # Implement your custom notification logic here + # For example, send a custom email, push notification, etc. + + return membership + except Exception as error: + print('Failed to invite user:', error) + raise error +``` +{% /multicode %} + +2. Implement custom notification logic: + - Send custom emails using [Appwrite's email service](/docs/products/messaging/email) + - Send push notifications + - Create in-app notifications + - Use webhooks to trigger external systems + +3. Handle invitation acceptance: + - Create custom UI for accepting/rejecting invites + - Implement your own invitation tracking system + - Add additional validation or requirements + +# Best practices {% #best-practices %} + +1. **Security** + - Only allow team owners to invite new members + - Validate user permissions before creating memberships + - Use appropriate API key scopes for server-side operations + +2. **User Experience** + - Provide clear feedback about invitation status + - Show pending invitations in your UI + - Allow canceling invitations before they're accepted + +3. **Error Handling** + - Handle cases where users are already team members + - Manage invitation expiration + - Provide clear error messages to users + +4. **Scalability** + - Use server-side invites for bulk operations + - Implement rate limiting for invitation requests + - Cache team membership data when appropriate \ No newline at end of file From 4af3356f082ca3f0f3b585d0ff0894fa4e831f6f Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Fri, 10 Jan 2025 20:57:31 +0000 Subject: [PATCH 02/19] Update doc --- .../products/auth/team-invites/+page.markdoc | 373 ++++++++++++------ 1 file changed, 253 insertions(+), 120 deletions(-) diff --git a/src/routes/docs/products/auth/team-invites/+page.markdoc b/src/routes/docs/products/auth/team-invites/+page.markdoc index 2f522d31ac..c58148d43b 100644 --- a/src/routes/docs/products/auth/team-invites/+page.markdoc +++ b/src/routes/docs/products/auth/team-invites/+page.markdoc @@ -4,11 +4,16 @@ title: Team Invites description: Learn how to manage team invites in Appwrite. Explore both client-side and server-side approaches for inviting users to teams. --- -Teams in Appwrite allow users to share access to resources. When you invite users to a team, they can access resources based on their team roles and permissions. There are two main approaches to handling team invites: client-side (using email) and server-side (for custom flows). +Teams in Appwrite allow users to share access to resources. When you invite users to a team, they can access resources based on their team roles and permissions. +There are two main approaches to handling team invites: client-side (using email) and server-side (for custom flows). # Client-side invites {% #client-side %} Client-side invites are the simplest way to invite users to a team. When you create a membership, Appwrite automatically sends an email invitation to the user. +This approach is ideal when: +- You want to use Appwrite's built-in email invitation system +- You need a quick and reliable way to invite users +- You want to leverage Appwrite's email templates and localization {% multicode %} ```client-web @@ -26,6 +31,23 @@ const membership = await teams.createMembership( ['developer'], // roles 'user@example.com' // email ); + +// You can also invite multiple users with different roles +const inviteUsers = async () => { + const invites = [ + { email: 'developer@example.com', roles: ['developer'] }, + { email: 'admin@example.com', roles: ['admin', 'developer'] }, + { email: 'viewer@example.com', roles: ['viewer'] } + ] + + for (const invite of invites) { + await teams.createMembership( + 'team_id', + invite.roles, + invite.email + ) + } +} ``` ```client-flutter import 'package:appwrite/appwrite.dart'; @@ -42,35 +64,112 @@ final membership = await teams.createMembership( roles: ['developer'], email: 'user@example.com' ); + +// You can also invite multiple users with different roles +Future inviteUsers() async { + final invites = [ + {'email': 'developer@example.com', 'roles': ['developer']}, + {'email': 'admin@example.com', 'roles': ['admin', 'developer']}, + {'email': 'viewer@example.com', 'roles': ['viewer']} + ]; + + for (final invite in invites) { + await teams.createMembership( + teamId: 'team_id', + roles: invite['roles'] as List, + email: invite['email'] as String + ); + } +} ``` {% /multicode %} -The invited user will receive an email with a link to accept the invitation. When they click the link, they'll be redirected to your app where you can handle the invitation acceptance: +## Handling invitation acceptance {% #handling-acceptance %} + +When users receive an invitation email, they'll get a link with the following query parameters: +- `teamId`: The ID of the team they're being invited to +- `membershipId`: The unique ID of their membership invitation +- `userId`: The user's ID (if they're already registered) +- `secret`: A secret token to verify the invitation + +Here's how to handle the invitation acceptance in your app: {% multicode %} ```client-web -// Handle the invitation acceptance -const membership = await teams.updateMembershipStatus( - 'team_id', - 'membership_id', - 'user_id', - 'secret' -); +// Create a component or page to handle invites +const AcceptInvite = () => { + const acceptInvitation = async () => { + // Get parameters from URL + const urlParams = new URLSearchParams(window.location.search) + const teamId = urlParams.get('teamId') + const membershipId = urlParams.get('membershipId') + const userId = urlParams.get('userId') + const secret = urlParams.get('secret') + + try { + const membership = await teams.updateMembershipStatus( + teamId, + membershipId, + userId, + secret + ) + // Handle successful acceptance + console.log('Invitation accepted:', membership) + } catch (error) { + // Handle errors (expired invite, already accepted, etc.) + console.error('Failed to accept invitation:', error) + } + } + + return ( + + ) +} ``` ```client-flutter -// Handle the invitation acceptance -final membership = await teams.updateMembershipStatus( - teamId: 'team_id', - membershipId: 'membership_id', - userId: 'user_id', - secret: 'secret' -); +class AcceptInvite extends StatelessWidget { + Future acceptInvitation(BuildContext context) async { + // Get parameters from deep link or navigation + final String teamId = '...'; + final String membershipId = '...'; + final String userId = '...'; + final String secret = '...'; + + try { + final membership = await teams.updateMembershipStatus( + teamId: teamId, + membershipId: membershipId, + userId: userId, + secret: secret + ); + // Handle successful acceptance + print('Invitation accepted: $membership'); + } catch (error) { + // Handle errors (expired invite, already accepted, etc.) + print('Failed to accept invitation: $error'); + } + } + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: () => acceptInvitation(context), + child: Text('Accept Team Invitation'), + ); + } +} ``` {% /multicode %} # Server-side invites {% #server-side %} -Server-side invites give you more control over the invitation process. Instead of sending automatic emails, you can create custom flows using the server SDK. +Server-side invites give you more control over the invitation process. Instead of sending automatic emails, you can create custom flows using the server SDK. This approach is particularly useful for: +- Custom invitation workflows +- Bulk user management +- Automated team assignments +- Integration with external systems {% multicode %} ```server-nodejs @@ -85,11 +184,46 @@ const teams = new sdk.Teams(client); // Create membership directly with userId const membership = await teams.createMembership( - teamId: 'team_id', - roles: ['developer'], - userId: 'user_id', - name: 'John Doe' // optional + 'team_id', + ['developer'], + 'user_id', + 'John Doe' // optional ); + +// Bulk invite users to multiple teams +async function bulkTeamInvites() { + const usersAndTeams = [ + { userId: 'user1', teamId: 'team1', roles: ['developer'] }, + { userId: 'user1', teamId: 'team2', roles: ['viewer'] }, + { userId: 'user2', teamId: 'team1', roles: ['admin'] } + ] + + for (const invite of usersAndTeams) { + await teams.createMembership( + invite.teamId, + invite.roles, + invite.userId + ) + } +} + +// Create team and add members in one go +async function createTeamWithMembers(teamName, members) { + // Create the team + const team = await teams.create('unique()', teamName); + + // Add members + for (const member of members) { + await teams.createMembership( + team.$id, + member.roles, + member.userId, + member.name + ); + } + + return team; +} ``` ```python from appwrite.client import Client @@ -109,137 +243,136 @@ membership = teams.create_membership( user_id='user_id', name='John Doe' # optional ) + +# Bulk invite users to multiple teams +async def bulk_team_invites(): + users_and_teams = [ + {'user_id': 'user1', 'team_id': 'team1', 'roles': ['developer']}, + {'user_id': 'user1', 'team_id': 'team2', 'roles': ['viewer']}, + {'user_id': 'user2', 'team_id': 'team1', 'roles': ['admin']} + ] + + for invite in users_and_teams: + teams.create_membership( + team_id=invite['team_id'], + roles=invite['roles'], + user_id=invite['user_id'] + ) + +# Create team and add members in one go +async def create_team_with_members(team_name, members): + # Create the team + team = teams.create('unique()', team_name) + + # Add members + for member in members: + teams.create_membership( + team_id=team['$id'], + roles=member['roles'], + user_id=member['user_id'], + name=member.get('name') + ) + + return team ``` {% /multicode %} -Server-side invites are useful when: -- You want to implement your own invitation email system -- You're building a custom invitation workflow -- You want to automatically add users to teams based on certain conditions -- You need to handle bulk invites efficiently +# Understanding roles and permissions {% #roles %} -# Understanding roles {% #roles %} +Team roles in Appwrite are flexible and can be used to create sophisticated access control systems. Here's a detailed look at how they work: -Team roles help you control what members can do within a team. When creating a membership, you can assign one or more roles: +## Built-in roles {% #built-in-roles %} 1. **owner** - Special role that can: - Invite new members - Remove members - Update team settings - Delete the team + - Automatically has all permissions within team scope + +The owner role is automatically assigned to the user who creates the team. Only users with the owner role can invite new members or assign the owner role to other members. -2. **Custom roles** - You define what these roles mean in your permissions: - - Can be any string (e.g., 'admin', 'developer', 'viewer') - - Used in combination with [permissions](/docs/advanced/platform/permissions) - - Multiple roles can be assigned to one member +## Custom roles {% #custom-roles %} -Example of using roles in permissions: +Custom roles can be any string that makes sense for your application. They are used to define granular permissions within your team. Here are some common patterns: ```javascript -// Grant access to users with 'developer' role in team_123 +// Common role patterns +const roles = { + administrative: ['admin', 'moderator', 'manager'], + access: ['reader', 'writer', 'viewer'], + department: ['engineering', 'marketing', 'sales'], + level: ['junior', 'senior', 'lead'] +} +``` + +## Role-based permissions {% #role-permissions %} + +Roles become powerful when combined with Appwrite's [permission system](/docs/advanced/platform/permissions). Here are the ways you can use team roles in permissions: + +```javascript +// Examples of role-based permissions + +// Grant access to specific role in a team ['team:team_123/developer'] -// Grant access to all members of team_123 +// Multiple roles for the same resource +['team:team_123/admin', 'team:team_123/developer'] + +// Access for any team member regardless of role ['team:team_123'] + +// Combine with resource permissions +['team:team_123/developer/documents.read'] +['team:team_123/admin/documents.*'] ``` -# Custom invitation flows {% #custom-flows %} +To gain access to team-based permissions: +1. A user must be invited and accept the team invitation +2. The user must have the specified role assigned to them +3. The role must be explicitly granted permission to the resource -Here's an example of implementing a custom invitation flow: +## Role hierarchy example {% #role-hierarchy %} -1. Create a server-side function to handle invites: +Here's how to implement a role hierarchy in your application: {% multicode %} ```server-nodejs -const sdk = require('node-appwrite'); - -async function inviteToTeam(teamId, userId, roles) { - const client = new sdk.Client() - .setEndpoint('https://cloud.appwrite.io/v1') - .setProject('') - .setKey(''); - - const teams = new sdk.Teams(client); - - try { - // Create membership - const membership = await teams.createMembership( - teamId, - roles, - userId, - 'John Doe' // optional name - ); +const roleHierarchy = { + admin: ['*'], // All permissions + manager: ['documents.read', 'documents.write', 'users.read'], + developer: ['documents.read', 'documents.write'], + viewer: ['documents.read'] +} - // Implement your custom notification logic here - // For example, send a custom email, push notification, etc. +async function hasPermission(userId, teamId, permission) { + const membership = await teams.getMembership(teamId, userId) + const userRoles = membership.roles - return membership; - } catch (error) { - console.error('Failed to invite user:', error); - throw error; - } + return userRoles.some(role => + roleHierarchy[role]?.includes('*') || + roleHierarchy[role]?.includes(permission) + ) } ``` ```python -from appwrite.client import Client -from appwrite.services.teams import Teams - -async def invite_to_team(team_id, user_id, roles): - client = Client() - client.set_endpoint('https://cloud.appwrite.io/v1') - client.set_project('') - client.set_key('') - - teams = Teams(client) - - try: - # Create membership - membership = teams.create_membership( - team_id=team_id, - roles=roles, - user_id=user_id, - name='John Doe' # optional - ) +role_hierarchy = { + 'admin': ['*'], # All permissions + 'manager': ['documents.read', 'documents.write', 'users.read'], + 'developer': ['documents.read', 'documents.write'], + 'viewer': ['documents.read'] +} - # Implement your custom notification logic here - # For example, send a custom email, push notification, etc. +async def has_permission(user_id, team_id, permission): + membership = teams.get_membership(team_id, user_id) + user_roles = membership.roles - return membership - except Exception as error: - print('Failed to invite user:', error) - raise error + return any( + '*' in role_hierarchy.get(role, []) or + permission in role_hierarchy.get(role, []) + for role in user_roles + ) ``` {% /multicode %} -2. Implement custom notification logic: - - Send custom emails using [Appwrite's email service](/docs/products/messaging/email) - - Send push notifications - - Create in-app notifications - - Use webhooks to trigger external systems - -3. Handle invitation acceptance: - - Create custom UI for accepting/rejecting invites - - Implement your own invitation tracking system - - Add additional validation or requirements - -# Best practices {% #best-practices %} - -1. **Security** - - Only allow team owners to invite new members - - Validate user permissions before creating memberships - - Use appropriate API key scopes for server-side operations - -2. **User Experience** - - Provide clear feedback about invitation status - - Show pending invitations in your UI - - Allow canceling invitations before they're accepted - -3. **Error Handling** - - Handle cases where users are already team members - - Manage invitation expiration - - Provide clear error messages to users - -4. **Scalability** - - Use server-side invites for bulk operations - - Implement rate limiting for invitation requests - - Cache team membership data when appropriate \ No newline at end of file +# Custom invitation flows {% #custom-flows %} \ No newline at end of file From a351be86baf95035f52ba41778407be2fb46252b Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Tue, 14 Jan 2025 15:59:35 +0000 Subject: [PATCH 03/19] Update article --- .../products/auth/team-invites/+page.markdoc | 117 +----------------- 1 file changed, 3 insertions(+), 114 deletions(-) diff --git a/src/routes/docs/products/auth/team-invites/+page.markdoc b/src/routes/docs/products/auth/team-invites/+page.markdoc index c58148d43b..4aa21692d8 100644 --- a/src/routes/docs/products/auth/team-invites/+page.markdoc +++ b/src/routes/docs/products/auth/team-invites/+page.markdoc @@ -31,23 +31,6 @@ const membership = await teams.createMembership( ['developer'], // roles 'user@example.com' // email ); - -// You can also invite multiple users with different roles -const inviteUsers = async () => { - const invites = [ - { email: 'developer@example.com', roles: ['developer'] }, - { email: 'admin@example.com', roles: ['admin', 'developer'] }, - { email: 'viewer@example.com', roles: ['viewer'] } - ] - - for (const invite of invites) { - await teams.createMembership( - 'team_id', - invite.roles, - invite.email - ) - } -} ``` ```client-flutter import 'package:appwrite/appwrite.dart'; @@ -64,108 +47,14 @@ final membership = await teams.createMembership( roles: ['developer'], email: 'user@example.com' ); - -// You can also invite multiple users with different roles -Future inviteUsers() async { - final invites = [ - {'email': 'developer@example.com', 'roles': ['developer']}, - {'email': 'admin@example.com', 'roles': ['admin', 'developer']}, - {'email': 'viewer@example.com', 'roles': ['viewer']} - ]; - - for (final invite in invites) { - await teams.createMembership( - teamId: 'team_id', - roles: invite['roles'] as List, - email: invite['email'] as String - ); - } -} -``` -{% /multicode %} - -## Handling invitation acceptance {% #handling-acceptance %} - -When users receive an invitation email, they'll get a link with the following query parameters: -- `teamId`: The ID of the team they're being invited to -- `membershipId`: The unique ID of their membership invitation -- `userId`: The user's ID (if they're already registered) -- `secret`: A secret token to verify the invitation - -Here's how to handle the invitation acceptance in your app: - -{% multicode %} -```client-web -// Create a component or page to handle invites -const AcceptInvite = () => { - const acceptInvitation = async () => { - // Get parameters from URL - const urlParams = new URLSearchParams(window.location.search) - const teamId = urlParams.get('teamId') - const membershipId = urlParams.get('membershipId') - const userId = urlParams.get('userId') - const secret = urlParams.get('secret') - - try { - const membership = await teams.updateMembershipStatus( - teamId, - membershipId, - userId, - secret - ) - // Handle successful acceptance - console.log('Invitation accepted:', membership) - } catch (error) { - // Handle errors (expired invite, already accepted, etc.) - console.error('Failed to accept invitation:', error) - } - } - - return ( - - ) -} -``` -```client-flutter -class AcceptInvite extends StatelessWidget { - Future acceptInvitation(BuildContext context) async { - // Get parameters from deep link or navigation - final String teamId = '...'; - final String membershipId = '...'; - final String userId = '...'; - final String secret = '...'; - - try { - final membership = await teams.updateMembershipStatus( - teamId: teamId, - membershipId: membershipId, - userId: userId, - secret: secret - ); - // Handle successful acceptance - print('Invitation accepted: $membership'); - } catch (error) { - // Handle errors (expired invite, already accepted, etc.) - print('Failed to accept invitation: $error'); - } - } - - @override - Widget build(BuildContext context) { - return ElevatedButton( - onPressed: () => acceptInvitation(context), - child: Text('Accept Team Invitation'), - ); - } -} ``` {% /multicode %} # Server-side invites {% #server-side %} -Server-side invites give you more control over the invitation process. Instead of sending automatic emails, you can create custom flows using the server SDK. This approach is particularly useful for: +Server-side invites give you more control over the invitation process. +Instead of sending automatic emails, you can create custom flows using the server SDK. +This approach is useful for: - Custom invitation workflows - Bulk user management - Automated team assignments From 8fa4287b159c0b85c292502ff5bd7cb2ae935c82 Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Tue, 14 Jan 2025 18:29:54 +0000 Subject: [PATCH 04/19] Update article --- .../products/auth/team-invites/+page.markdoc | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/routes/docs/products/auth/team-invites/+page.markdoc b/src/routes/docs/products/auth/team-invites/+page.markdoc index 4aa21692d8..aa6332d4c2 100644 --- a/src/routes/docs/products/auth/team-invites/+page.markdoc +++ b/src/routes/docs/products/auth/team-invites/+page.markdoc @@ -79,23 +79,6 @@ const membership = await teams.createMembership( 'John Doe' // optional ); -// Bulk invite users to multiple teams -async function bulkTeamInvites() { - const usersAndTeams = [ - { userId: 'user1', teamId: 'team1', roles: ['developer'] }, - { userId: 'user1', teamId: 'team2', roles: ['viewer'] }, - { userId: 'user2', teamId: 'team1', roles: ['admin'] } - ] - - for (const invite of usersAndTeams) { - await teams.createMembership( - invite.teamId, - invite.roles, - invite.userId - ) - } -} - // Create team and add members in one go async function createTeamWithMembers(teamName, members) { // Create the team From 530e4e3203cefd3e6b949a02a36ed5c45ac73cb9 Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Tue, 14 Jan 2025 18:31:59 +0000 Subject: [PATCH 05/19] Update article --- .../products/auth/team-invites/+page.markdoc | 76 +------------------ 1 file changed, 1 insertion(+), 75 deletions(-) diff --git a/src/routes/docs/products/auth/team-invites/+page.markdoc b/src/routes/docs/products/auth/team-invites/+page.markdoc index aa6332d4c2..97bf272f60 100644 --- a/src/routes/docs/products/auth/team-invites/+page.markdoc +++ b/src/routes/docs/products/auth/team-invites/+page.markdoc @@ -116,21 +116,6 @@ membership = teams.create_membership( name='John Doe' # optional ) -# Bulk invite users to multiple teams -async def bulk_team_invites(): - users_and_teams = [ - {'user_id': 'user1', 'team_id': 'team1', 'roles': ['developer']}, - {'user_id': 'user1', 'team_id': 'team2', 'roles': ['viewer']}, - {'user_id': 'user2', 'team_id': 'team1', 'roles': ['admin']} - ] - - for invite in users_and_teams: - teams.create_membership( - team_id=invite['team_id'], - roles=invite['roles'], - user_id=invite['user_id'] - ) - # Create team and add members in one go async def create_team_with_members(team_name, members): # Create the team @@ -164,20 +149,6 @@ Team roles in Appwrite are flexible and can be used to create sophisticated acce The owner role is automatically assigned to the user who creates the team. Only users with the owner role can invite new members or assign the owner role to other members. -## Custom roles {% #custom-roles %} - -Custom roles can be any string that makes sense for your application. They are used to define granular permissions within your team. Here are some common patterns: - -```javascript -// Common role patterns -const roles = { - administrative: ['admin', 'moderator', 'manager'], - access: ['reader', 'writer', 'viewer'], - department: ['engineering', 'marketing', 'sales'], - level: ['junior', 'senior', 'lead'] -} -``` - ## Role-based permissions {% #role-permissions %} Roles become powerful when combined with Appwrite's [permission system](/docs/advanced/platform/permissions). Here are the ways you can use team roles in permissions: @@ -202,49 +173,4 @@ Roles become powerful when combined with Appwrite's [permission system](/docs/ad To gain access to team-based permissions: 1. A user must be invited and accept the team invitation 2. The user must have the specified role assigned to them -3. The role must be explicitly granted permission to the resource - -## Role hierarchy example {% #role-hierarchy %} - -Here's how to implement a role hierarchy in your application: - -{% multicode %} -```server-nodejs -const roleHierarchy = { - admin: ['*'], // All permissions - manager: ['documents.read', 'documents.write', 'users.read'], - developer: ['documents.read', 'documents.write'], - viewer: ['documents.read'] -} - -async function hasPermission(userId, teamId, permission) { - const membership = await teams.getMembership(teamId, userId) - const userRoles = membership.roles - - return userRoles.some(role => - roleHierarchy[role]?.includes('*') || - roleHierarchy[role]?.includes(permission) - ) -} -``` -```python -role_hierarchy = { - 'admin': ['*'], # All permissions - 'manager': ['documents.read', 'documents.write', 'users.read'], - 'developer': ['documents.read', 'documents.write'], - 'viewer': ['documents.read'] -} - -async def has_permission(user_id, team_id, permission): - membership = teams.get_membership(team_id, user_id) - user_roles = membership.roles - - return any( - '*' in role_hierarchy.get(role, []) or - permission in role_hierarchy.get(role, []) - for role in user_roles - ) -``` -{% /multicode %} - -# Custom invitation flows {% #custom-flows %} \ No newline at end of file +3. The role must be explicitly granted permission to the resource \ No newline at end of file From aa00b2ae9d9ab4846e21cf5fd2e7235140762824 Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Tue, 14 Jan 2025 18:42:52 +0000 Subject: [PATCH 06/19] Update article --- .../products/auth/team-invites/+page.markdoc | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/src/routes/docs/products/auth/team-invites/+page.markdoc b/src/routes/docs/products/auth/team-invites/+page.markdoc index 97bf272f60..2ef9b7433d 100644 --- a/src/routes/docs/products/auth/team-invites/+page.markdoc +++ b/src/routes/docs/products/auth/team-invites/+page.markdoc @@ -78,24 +78,6 @@ const membership = await teams.createMembership( 'user_id', 'John Doe' // optional ); - -// Create team and add members in one go -async function createTeamWithMembers(teamName, members) { - // Create the team - const team = await teams.create('unique()', teamName); - - // Add members - for (const member of members) { - await teams.createMembership( - team.$id, - member.roles, - member.userId, - member.name - ); - } - - return team; -} ``` ```python from appwrite.client import Client @@ -115,22 +97,6 @@ membership = teams.create_membership( user_id='user_id', name='John Doe' # optional ) - -# Create team and add members in one go -async def create_team_with_members(team_name, members): - # Create the team - team = teams.create('unique()', team_name) - - # Add members - for member in members: - teams.create_membership( - team_id=team['$id'], - roles=member['roles'], - user_id=member['user_id'], - name=member.get('name') - ) - - return team ``` {% /multicode %} From e785e393b9493ad193f2650bc4f9c43f464292e0 Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Tue, 14 Jan 2025 19:49:27 +0000 Subject: [PATCH 07/19] Add to user journey --- src/routes/docs/products/auth/+layout.svelte | 4 ++++ src/routes/docs/products/auth/+page.markdoc | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/routes/docs/products/auth/+layout.svelte b/src/routes/docs/products/auth/+layout.svelte index aa34ac1014..87e1faa96f 100644 --- a/src/routes/docs/products/auth/+layout.svelte +++ b/src/routes/docs/products/auth/+layout.svelte @@ -96,6 +96,10 @@ { label: 'Verify user', href: '/docs/products/auth/verify-user' + }, + { + label: 'Team invites', + href: '/docs/products/auth/team-invites' } ] }, diff --git a/src/routes/docs/products/auth/+page.markdoc b/src/routes/docs/products/auth/+page.markdoc index dd338123e6..976e539f64 100644 --- a/src/routes/docs/products/auth/+page.markdoc +++ b/src/routes/docs/products/auth/+page.markdoc @@ -46,10 +46,13 @@ Implement custom authentication methods like biometric and passkey login by gene {% cards_item href="/docs/products/auth/mfa" title="Multifactor authentication (MFA)" %} Implementing MFA to add extra layers of security to your app. {% /cards_item %} +{% cards_item href="/docs/products/auth/team-invites" title="Team invites" %} +Manage team invites and memberships with client-side and server-side approaches. +{% /cards_item %} {% /cards %} # Flexible permissions {% #flexible-permissions %} -When users sign up using Appwrite, their identity is automatically attached to a robust permissions system. +When users sign up using Appwrite, their identity is automatically attached to a robust permissions system. Appwrite Authentication provides permissions for individual users and groups of users through [teams](/docs/products/auth/teams) and [labels](/docs/products/auth/labels). # Built in preferences {% #built-in-preferences %} From fca2283cc460fa7318077d1f12d0152e0c2d3ad2 Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Tue, 14 Jan 2025 20:50:00 +0000 Subject: [PATCH 08/19] Add to user journey --- .../products/auth/team-invites/+page.markdoc | 145 +++++++++++++----- 1 file changed, 106 insertions(+), 39 deletions(-) diff --git a/src/routes/docs/products/auth/team-invites/+page.markdoc b/src/routes/docs/products/auth/team-invites/+page.markdoc index 2ef9b7433d..7f9cc5e7c9 100644 --- a/src/routes/docs/products/auth/team-invites/+page.markdoc +++ b/src/routes/docs/products/auth/team-invites/+page.markdoc @@ -1,19 +1,18 @@ --- layout: article -title: Team Invites -description: Learn how to manage team invites in Appwrite. Explore both client-side and server-side approaches for inviting users to teams. +title: Team invites +description: Learn how to manage team invites in Appwrite. Implement both client-side email invites and server-side custom flows for team memberships. --- -Teams in Appwrite allow users to share access to resources. When you invite users to a team, they can access resources based on their team roles and permissions. -There are two main approaches to handling team invites: client-side (using email) and server-side (for custom flows). +Teams in Appwrite let users share access to resources through roles and permissions. There are two ways to handle team invites: client-side with email invites and server-side for custom flows. -# Client-side invites {% #client-side %} +# Client-side email invites {% #client-side %} -Client-side invites are the simplest way to invite users to a team. When you create a membership, Appwrite automatically sends an email invitation to the user. -This approach is ideal when: -- You want to use Appwrite's built-in email invitation system -- You need a quick and reliable way to invite users -- You want to leverage Appwrite's email templates and localization +Client-side invites use Appwrite's built-in email system. When you create a membership, Appwrite automatically sends an email invitation to the user. +Use this approach when you want: +- Automatic email invitations +- Built-in email templates and localization +- Simple invite acceptance flow {% multicode %} ```client-web @@ -25,7 +24,7 @@ const client = new Client() const teams = new Teams(client); -// Invite a user by email +// Create membership with email invite const membership = await teams.createMembership( 'team_id', ['developer'], // roles @@ -41,20 +40,52 @@ final client = Client() final teams = Teams(client); -// Invite a user by email +// Create membership with email invite final membership = await teams.createMembership( teamId: 'team_id', roles: ['developer'], email: 'user@example.com' ); ``` +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +let teams = Teams(client) + +// Create membership with email invite +let membership = try await teams.createMembership( + teamId: "team_id", + roles: ["developer"], + email: "user@example.com" +) +``` +```kotlin +import io.appwrite.Client +import io.appwrite.services.Teams + +val client = Client(context) + .setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val teams = Teams(client) + +// Create membership with email invite +val response = teams.createMembership( + teamId = "team_id", + roles = listOf("developer"), + email = "user@example.com" +) +``` {% /multicode %} -# Server-side invites {% #server-side %} +# Server-side custom flows {% #server-side %} -Server-side invites give you more control over the invitation process. -Instead of sending automatic emails, you can create custom flows using the server SDK. -This approach is useful for: +Server-side invites give you complete control over the invitation process. Instead of using email invites, you create memberships directly using user IDs. +This approach is essential for: - Custom invitation workflows - Bulk user management - Automated team assignments @@ -76,10 +107,10 @@ const membership = await teams.createMembership( 'team_id', ['developer'], 'user_id', - 'John Doe' // optional + 'John Doe' // optional name ); ``` -```python +```server-python from appwrite.client import Client from appwrite.services.teams import Teams @@ -98,45 +129,81 @@ membership = teams.create_membership( name='John Doe' # optional ) ``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + .setKey("") // Your secret API key + +let teams = Teams(client) + +// Create membership directly with userId +let membership = try await teams.createMembership( + teamId: "team_id", + roles: ["developer"], + userId: "user_id", + name: "John Doe" // optional +) +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Teams + +val client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + .setKey("") // Your secret API key + +val teams = Teams(client) + +// Create membership directly with userId +val response = teams.createMembership( + teamId = "team_id", + roles = listOf("developer"), + userId = "user_id", + name = "John Doe" // optional +) +``` {% /multicode %} -# Understanding roles and permissions {% #roles %} +# Membership roles {% #roles %} -Team roles in Appwrite are flexible and can be used to create sophisticated access control systems. Here's a detailed look at how they work: +Membership roles define what team members can do. Understanding roles is crucial for implementing proper access control. -## Built-in roles {% #built-in-roles %} +## Built-in owner role {% #owner-role %} -1. **owner** - Special role that can: - - Invite new members - - Remove members - - Update team settings - - Delete the team - - Automatically has all permissions within team scope +The `owner` role is special and has full control. Team owners can: +- Invite new members +- Remove members +- Update team settings +- Delete the team +- Access everything in the team's scope -The owner role is automatically assigned to the user who creates the team. Only users with the owner role can invite new members or assign the owner role to other members. +The user who creates a team automatically becomes its owner. Only owners can invite new members or assign the owner role to others. -## Role-based permissions {% #role-permissions %} +## Role-based permissions {% #permissions %} -Roles become powerful when combined with Appwrite's [permission system](/docs/advanced/platform/permissions). Here are the ways you can use team roles in permissions: +Roles integrate with Appwrite's [permission system](/docs/advanced/platform/permissions) to control resource access. Here are common permission patterns: ```javascript -// Examples of role-based permissions - -// Grant access to specific role in a team +// Grant access to specific roles ['team:team_123/developer'] +['team:team_123/admin'] // Multiple roles for the same resource ['team:team_123/admin', 'team:team_123/developer'] -// Access for any team member regardless of role +// Access for any team member ['team:team_123'] -// Combine with resource permissions +// Granular resource permissions ['team:team_123/developer/documents.read'] ['team:team_123/admin/documents.*'] ``` -To gain access to team-based permissions: -1. A user must be invited and accept the team invitation -2. The user must have the specified role assigned to them -3. The role must be explicitly granted permission to the resource \ No newline at end of file +For team permissions to work: +1. For client-side invites, the user must accept the email invitation. For server-side flows, the membership is created directly +2. The user must have the required role +3. The role must have explicit permission to the resource \ No newline at end of file From a93e9167e7e412cb6ea351be7ecb34658e22e77b Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Tue, 14 Jan 2025 21:04:38 +0000 Subject: [PATCH 09/19] Add to user journey --- .../products/auth/team-invites/+page.markdoc | 250 +++++++++++++++--- 1 file changed, 219 insertions(+), 31 deletions(-) diff --git a/src/routes/docs/products/auth/team-invites/+page.markdoc b/src/routes/docs/products/auth/team-invites/+page.markdoc index 7f9cc5e7c9..6a7cb1d6ad 100644 --- a/src/routes/docs/products/auth/team-invites/+page.markdoc +++ b/src/routes/docs/products/auth/team-invites/+page.markdoc @@ -4,15 +4,20 @@ title: Team invites description: Learn how to manage team invites in Appwrite. Implement both client-side email invites and server-side custom flows for team memberships. --- -Teams in Appwrite let users share access to resources through roles and permissions. There are two ways to handle team invites: client-side with email invites and server-side for custom flows. +Appwrite provides two approaches for adding members to teams: client-side email invites and server-side custom flows. Each approach serves different use cases and offers unique benefits. # Client-side email invites {% #client-side %} -Client-side invites use Appwrite's built-in email system. When you create a membership, Appwrite automatically sends an email invitation to the user. +Client-side email invites provide a user-friendly way to add members to your team. When creating a membership, Appwrite: +1. Sends an automated email invitation to the user +2. Creates a pending membership +3. Activates the membership when the user accepts + Use this approach when you want: - Automatic email invitations - Built-in email templates and localization - Simple invite acceptance flow +- User verification through email {% multicode %} ```client-web @@ -84,12 +89,17 @@ val response = teams.createMembership( # Server-side custom flows {% #server-side %} -Server-side invites give you complete control over the invitation process. Instead of using email invites, you create memberships directly using user IDs. -This approach is essential for: +Server-side membership creation bypasses the email invitation process, allowing direct member addition. This approach: +1. Creates an active membership immediately +2. Doesn't require user acceptance +3. Gives you complete control over the invitation workflow + +Use this when you need: - Custom invitation workflows - Bulk user management - Automated team assignments - Integration with external systems +- Immediate member access {% multicode %} ```server-nodejs @@ -168,42 +178,220 @@ val response = teams.createMembership( ``` {% /multicode %} -# Membership roles {% #roles %} +# Managing memberships {% #managing %} -Membership roles define what team members can do. Understanding roles is crucial for implementing proper access control. +## Accepting invitations {% #accepting %} -## Built-in owner role {% #owner-role %} +For client-side email invites, users must accept the invitation to join the team. The acceptance flow: +1. User receives an email with an invitation link +2. Clicking the link redirects to your app +3. Your app calls the acceptance endpoint +4. Upon success, the user gains immediate access -The `owner` role is special and has full control. Team owners can: -- Invite new members -- Remove members -- Update team settings -- Delete the team -- Access everything in the team's scope +{% multicode %} +```client-web +import { Client, Teams } from "appwrite"; -The user who creates a team automatically becomes its owner. Only owners can invite new members or assign the owner role to others. +const client = new Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); -## Role-based permissions {% #permissions %} +const teams = new Teams(client); -Roles integrate with Appwrite's [permission system](/docs/advanced/platform/permissions) to control resource access. Here are common permission patterns: +// Accept the invitation using the membership ID +const response = await teams.updateMembershipStatus( + 'team_id', + 'membership_id', + 'user_id' +); +``` +```client-flutter +import 'package:appwrite/appwrite.dart'; -```javascript -// Grant access to specific roles -['team:team_123/developer'] -['team:team_123/admin'] +final client = Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); -// Multiple roles for the same resource -['team:team_123/admin', 'team:team_123/developer'] +final teams = Teams(client); -// Access for any team member -['team:team_123'] +// Accept the invitation using the membership ID +final response = await teams.updateMembershipStatus( + teamId: 'team_id', + membershipId: 'membership_id', + userId: 'user_id' +); +``` +```client-apple +import Appwrite -// Granular resource permissions -['team:team_123/developer/documents.read'] -['team:team_123/admin/documents.*'] +let client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +let teams = Teams(client) + +// Accept the invitation using the membership ID +let response = try await teams.updateMembershipStatus( + teamId: "team_id", + membershipId: "membership_id", + userId: "user_id" +) ``` +```kotlin +import io.appwrite.Client +import io.appwrite.services.Teams + +val client = Client(context) + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +val teams = Teams(client) + +// Accept the invitation using the membership ID +val response = teams.updateMembershipStatus( + teamId = "team_id", + membershipId = "membership_id", + userId = "user_id" +) +``` +{% /multicode %} + +## Checking membership status {% #status %} + +You can verify a user's membership status to determine if they've accepted an invitation: + +{% multicode %} +```client-web +import { Client, Teams } from "appwrite"; + +const client = new Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); + +const teams = new Teams(client); + +const membership = await teams.getMembership( + 'team_id', + 'membership_id' +); + +console.log(membership.status); // 'invited' or 'confirmed' +``` +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); + +final teams = Teams(client); + +final membership = await teams.getMembership( + teamId: 'team_id', + membershipId: 'membership_id' +); + +print(membership.status); // 'invited' or 'confirmed' +``` +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +let teams = Teams(client) + +let membership = try await teams.getMembership( + teamId: "team_id", + membershipId: "membership_id" +) + +print(membership.status) // 'invited' or 'confirmed' +``` +```kotlin +import io.appwrite.Client +import io.appwrite.services.Teams + +val client = Client(context) + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +val teams = Teams(client) + +val membership = teams.getMembership( + teamId = "team_id", + membershipId = "membership_id" +) + +println(membership.status) // 'invited' or 'confirmed' +``` +{% /multicode %} + +## Removing members {% #removing %} + +Team owners can remove members or users can leave teams: + +{% multicode %} +```client-web +import { Client, Teams } from "appwrite"; + +const client = new Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); + +const teams = new Teams(client); + +await teams.deleteMembership( + 'team_id', + 'membership_id' +); +``` +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); + +final teams = Teams(client); + +await teams.deleteMembership( + teamId: 'team_id', + membershipId: 'membership_id' +); +``` +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +let teams = Teams(client) + +try await teams.deleteMembership( + teamId: "team_id", + membershipId: "membership_id" +) +``` +```kotlin +import io.appwrite.Client +import io.appwrite.services.Teams + +val client = Client(context) + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +val teams = Teams(client) + +teams.deleteMembership( + teamId = "team_id", + membershipId = "membership_id" +) +``` +{% /multicode %} -For team permissions to work: -1. For client-side invites, the user must accept the email invitation. For server-side flows, the membership is created directly -2. The user must have the required role -3. The role must have explicit permission to the resource \ No newline at end of file +{% arrow_link href="/docs/products/auth/teams" %} +Learn more about team management +{% /arrow_link %} \ No newline at end of file From 6e85af756d2b00b88771df9c92b351c6e498f090 Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Tue, 14 Jan 2025 21:36:52 +0000 Subject: [PATCH 10/19] Update article --- .../products/auth/team-invites/+page.markdoc | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/routes/docs/products/auth/team-invites/+page.markdoc b/src/routes/docs/products/auth/team-invites/+page.markdoc index 6a7cb1d6ad..a5aa0091d1 100644 --- a/src/routes/docs/products/auth/team-invites/+page.markdoc +++ b/src/routes/docs/products/auth/team-invites/+page.markdoc @@ -13,11 +13,8 @@ Client-side email invites provide a user-friendly way to add members to your tea 2. Creates a pending membership 3. Activates the membership when the user accepts -Use this approach when you want: -- Automatic email invitations -- Built-in email templates and localization -- Simple invite acceptance flow -- User verification through email +Client-side invites are ideal when you want a simple, automated process. +Appwrite handles the email delivery with built-in templates and localization support, making it easy to implement a standard invite acceptance flow with email verification. {% multicode %} ```client-web @@ -94,12 +91,9 @@ Server-side membership creation bypasses the email invitation process, allowing 2. Doesn't require user acceptance 3. Gives you complete control over the invitation workflow -Use this when you need: -- Custom invitation workflows -- Bulk user management -- Automated team assignments -- Integration with external systems -- Immediate member access +Server-side flows give you complete control over the membership process. +This makes them perfect for scenarios requiring custom workflows, such as bulk user management, automated team assignments, or integration with external systems. +Since memberships are created directly, users gain immediate access without waiting for email acceptance. {% multicode %} ```server-nodejs @@ -178,9 +172,9 @@ val response = teams.createMembership( ``` {% /multicode %} -# Managing memberships {% #managing %} +# Manage memberships {% #manage-memberships %} -## Accepting invitations {% #accepting %} +## Accept invitations {% #accept-invitations %} For client-side email invites, users must accept the invitation to join the team. The acceptance flow: 1. User receives an email with an invitation link @@ -256,7 +250,7 @@ val response = teams.updateMembershipStatus( ``` {% /multicode %} -## Checking membership status {% #status %} +## Check membership status {% #status %} You can verify a user's membership status to determine if they've accepted an invitation: @@ -328,7 +322,7 @@ println(membership.status) // 'invited' or 'confirmed' ``` {% /multicode %} -## Removing members {% #removing %} +## Remove members {% #remove-members %} Team owners can remove members or users can leave teams: From e00b5dc9cca21cca58ac6b1765ae7342700ea520 Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Mon, 27 Jan 2025 18:51:26 +0000 Subject: [PATCH 11/19] mention a user is created if not existing --- src/routes/docs/products/auth/team-invites/+page.markdoc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/routes/docs/products/auth/team-invites/+page.markdoc b/src/routes/docs/products/auth/team-invites/+page.markdoc index a5aa0091d1..e300276a90 100644 --- a/src/routes/docs/products/auth/team-invites/+page.markdoc +++ b/src/routes/docs/products/auth/team-invites/+page.markdoc @@ -9,9 +9,10 @@ Appwrite provides two approaches for adding members to teams: client-side email # Client-side email invites {% #client-side %} Client-side email invites provide a user-friendly way to add members to your team. When creating a membership, Appwrite: -1. Sends an automated email invitation to the user -2. Creates a pending membership -3. Activates the membership when the user accepts +1. Creates a new user account if one doesn't exist for the email address +2. Sends an automated email invitation to the user +3. Creates a pending membership +4. Activates the membership when the user accepts Client-side invites are ideal when you want a simple, automated process. Appwrite handles the email delivery with built-in templates and localization support, making it easy to implement a standard invite acceptance flow with email verification. From e37f895dfff1b70b92f9fa95657af44b9846732b Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Mon, 27 Jan 2025 18:54:48 +0000 Subject: [PATCH 12/19] Emphasize user-to-user invite --- src/routes/docs/products/auth/team-invites/+page.markdoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/docs/products/auth/team-invites/+page.markdoc b/src/routes/docs/products/auth/team-invites/+page.markdoc index e300276a90..e085f9a72b 100644 --- a/src/routes/docs/products/auth/team-invites/+page.markdoc +++ b/src/routes/docs/products/auth/team-invites/+page.markdoc @@ -8,13 +8,13 @@ Appwrite provides two approaches for adding members to teams: client-side email # Client-side email invites {% #client-side %} -Client-side email invites provide a user-friendly way to add members to your team. When creating a membership, Appwrite: +Client-side email invites are perfect for implementing user-to-user invitations, allowing your users to invite others to join their teams, organizations, or shared resources. When creating a membership, Appwrite: 1. Creates a new user account if one doesn't exist for the email address 2. Sends an automated email invitation to the user 3. Creates a pending membership 4. Activates the membership when the user accepts -Client-side invites are ideal when you want a simple, automated process. +Client-side invites are ideal when you want a simple, automated process that lets your users manage their own team invitations. Appwrite handles the email delivery with built-in templates and localization support, making it easy to implement a standard invite acceptance flow with email verification. {% multicode %} From 05c13c85608624f07aa721f8c7093d5270f65841 Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Mon, 27 Jan 2025 18:56:48 +0000 Subject: [PATCH 13/19] Move accept invite to client-side section --- .../products/auth/team-invites/+page.markdoc | 152 +++++++++--------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/src/routes/docs/products/auth/team-invites/+page.markdoc b/src/routes/docs/products/auth/team-invites/+page.markdoc index e085f9a72b..99c17736c0 100644 --- a/src/routes/docs/products/auth/team-invites/+page.markdoc +++ b/src/routes/docs/products/auth/team-invites/+page.markdoc @@ -85,6 +85,82 @@ val response = teams.createMembership( ``` {% /multicode %} +## Accept invitations {% #accept-invitations %} + +For client-side email invites, users must accept the invitation to join the team. The acceptance flow: +1. User receives an email with an invitation link +2. Clicking the link redirects to your app +3. Your app calls the acceptance endpoint +4. Upon success, the user gains immediate access + +{% multicode %} +```client-web +import { Client, Teams } from "appwrite"; + +const client = new Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); + +const teams = new Teams(client); + +// Accept the invitation using the membership ID +const response = await teams.updateMembershipStatus( + 'team_id', + 'membership_id', + 'user_id' +); +``` +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); + +final teams = Teams(client); + +// Accept the invitation using the membership ID +final response = await teams.updateMembershipStatus( + teamId: 'team_id', + membershipId: 'membership_id', + userId: 'user_id' +); +``` +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +let teams = Teams(client) + +// Accept the invitation using the membership ID +let response = try await teams.updateMembershipStatus( + teamId: "team_id", + membershipId: "membership_id", + userId: "user_id" +) +``` +```kotlin +import io.appwrite.Client +import io.appwrite.services.Teams + +val client = Client(context) + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +val teams = Teams(client) + +// Accept the invitation using the membership ID +val response = teams.updateMembershipStatus( + teamId = "team_id", + membershipId = "membership_id", + userId = "user_id" +) +``` +{% /multicode %} + # Server-side custom flows {% #server-side %} Server-side membership creation bypasses the email invitation process, allowing direct member addition. This approach: @@ -175,82 +251,6 @@ val response = teams.createMembership( # Manage memberships {% #manage-memberships %} -## Accept invitations {% #accept-invitations %} - -For client-side email invites, users must accept the invitation to join the team. The acceptance flow: -1. User receives an email with an invitation link -2. Clicking the link redirects to your app -3. Your app calls the acceptance endpoint -4. Upon success, the user gains immediate access - -{% multicode %} -```client-web -import { Client, Teams } from "appwrite"; - -const client = new Client() - .setEndpoint('https://cloud.appwrite.io/v1') - .setProject(''); - -const teams = new Teams(client); - -// Accept the invitation using the membership ID -const response = await teams.updateMembershipStatus( - 'team_id', - 'membership_id', - 'user_id' -); -``` -```client-flutter -import 'package:appwrite/appwrite.dart'; - -final client = Client() - .setEndpoint('https://cloud.appwrite.io/v1') - .setProject(''); - -final teams = Teams(client); - -// Accept the invitation using the membership ID -final response = await teams.updateMembershipStatus( - teamId: 'team_id', - membershipId: 'membership_id', - userId: 'user_id' -); -``` -```client-apple -import Appwrite - -let client = Client() - .setEndpoint("https://cloud.appwrite.io/v1") - .setProject("") - -let teams = Teams(client) - -// Accept the invitation using the membership ID -let response = try await teams.updateMembershipStatus( - teamId: "team_id", - membershipId: "membership_id", - userId: "user_id" -) -``` -```kotlin -import io.appwrite.Client -import io.appwrite.services.Teams - -val client = Client(context) - .setEndpoint("https://cloud.appwrite.io/v1") - .setProject("") - -val teams = Teams(client) - -// Accept the invitation using the membership ID -val response = teams.updateMembershipStatus( - teamId = "team_id", - membershipId = "membership_id", - userId = "user_id" -) -``` -{% /multicode %} - ## Check membership status {% #status %} You can verify a user's membership status to determine if they've accepted an invitation: From aea812214a76fa7737aa4f967b75da3cddfa0247 Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Mon, 27 Jan 2025 20:39:23 +0000 Subject: [PATCH 14/19] Update check membership status code --- .../products/auth/team-invites/+page.markdoc | 195 ++++++++++++++++-- 1 file changed, 178 insertions(+), 17 deletions(-) diff --git a/src/routes/docs/products/auth/team-invites/+page.markdoc b/src/routes/docs/products/auth/team-invites/+page.markdoc index 99c17736c0..480a839ac5 100644 --- a/src/routes/docs/products/auth/team-invites/+page.markdoc +++ b/src/routes/docs/products/auth/team-invites/+page.markdoc @@ -253,7 +253,8 @@ val response = teams.createMembership( ## Check membership status {% #status %} -You can verify a user's membership status to determine if they've accepted an invitation: +### Client-side +To check membership status client-side, first list the teams and then get the memberships for a specific team: {% multicode %} ```client-web @@ -265,12 +266,21 @@ const client = new Client() const teams = new Teams(client); -const membership = await teams.getMembership( - 'team_id', - 'membership_id' +// Get list of teams the user is part of +const teamsList = await teams.list(); + +// For a specific team, get all memberships +const response = await teams.listMemberships('team_id'); + +// Find membership for specific user +const userMembership = response.memberships.find( + membership => membership.userId === 'user_id' ); -console.log(membership.status); // 'invited' or 'confirmed' +if (userMembership) { + console.log(userMembership.confirm); // false = invited, true = joined + console.log(userMembership.roles); // ['developer', etc] +} ``` ```client-flutter import 'package:appwrite/appwrite.dart'; @@ -281,12 +291,24 @@ final client = Client() final teams = Teams(client); -final membership = await teams.getMembership( - teamId: 'team_id', - membershipId: 'membership_id' +// Get list of teams the user is part of +final teamsList = await teams.list(); + +// For a specific team, get all memberships +final response = await teams.listMemberships( + teamId: 'team_id' ); -print(membership.status); // 'invited' or 'confirmed' +// Find membership for specific user +final userMembership = response.memberships.firstWhere( + (membership) => membership.userId == 'user_id', + orElse: () => null +); + +if (userMembership != null) { + print(userMembership.confirm); // false = invited, true = joined + print(userMembership.roles); // ['developer', etc] +} ``` ```client-apple import Appwrite @@ -297,12 +319,19 @@ let client = Client() let teams = Teams(client) -let membership = try await teams.getMembership( - teamId: "team_id", - membershipId: "membership_id" +// Get list of teams the user is part of +let teamsList = try await teams.list() + +// For a specific team, get all memberships +let response = try await teams.listMemberships( + teamId: "team_id" ) -print(membership.status) // 'invited' or 'confirmed' +// Find membership for specific user +if let userMembership = response.memberships.first(where: { $0.userId == "user_id" }) { + print(userMembership.confirm) // false = invited, true = joined + print(userMembership.roles) // ['developer', etc] +} ``` ```kotlin import io.appwrite.Client @@ -314,12 +343,144 @@ val client = Client(context) val teams = Teams(client) -val membership = teams.getMembership( - teamId = "team_id", - membershipId = "membership_id" +// Get list of teams the user is part of +val teamsList = teams.list() + +// For a specific team, get all memberships +val response = teams.listMemberships( + teamId = "team_id" ) -println(membership.status) // 'invited' or 'confirmed' +// Find membership for specific user +val userMembership = response.memberships.find { + it.userId == "user_id" +} + +userMembership?.let { + println(it.confirm) // false = invited, true = joined + println(it.roles) // ['developer', etc] +} +``` +{% /multicode %} + +### Server-side +Server-side requires iterating through teams and memberships since the data isn't filtered for a specific user: + +{% multicode %} +```server-nodejs +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const teams = new sdk.Teams(client); + +// Get all teams +const teamsList = await teams.list(); + +// Iterate through teams to find memberships +for (const team of teamsList.teams) { + const response = await teams.listMemberships(team.$id); + + // Find membership for specific user + const userMembership = response.memberships.find( + membership => membership.userId === 'user_id' + ); + + if (userMembership) { + console.log(`Team: ${team.name}`); + console.log(`Joined: ${userMembership.joined}`); // null if invited, timestamp if joined + console.log(`Roles: ${userMembership.roles}`); + } +} +``` +```server-python +from appwrite.client import Client +from appwrite.services.teams import Teams + +client = Client() +client.set_endpoint('https://cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +teams = Teams(client) + +// Get all teams +teams_list = teams.list() + +// Iterate through teams to find memberships +for team in teams_list['teams']: + response = teams.list_memberships(team['$id']) + + // Find membership for specific user + user_membership = next( + (m for m in response['memberships'] if m['userId'] == 'user_id'), + None + ) + + if user_membership: + print(f"Team: {team['name']}") + print(f"Joined: {user_membership['joined']}") # null if invited, timestamp if joined + print(f"Roles: {user_membership['roles']}") +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let teams = Teams(client) + +// Get all teams +let teamsList = try await teams.list() + +// Iterate through teams to find memberships +for team in teamsList.teams { + let response = try await teams.listMemberships( + teamId: team.$id + ) + + // Find membership for specific user + if let userMembership = response.memberships.first(where: { $0.userId == "user_id" }) { + print("Team: \(team.name)") + print("Joined: \(userMembership.joined)") # null if invited, timestamp if joined + print("Roles: \(userMembership.roles)") + } +} +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Teams + +val client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val teams = Teams(client) + +// Get all teams +val teamsList = teams.list() + +// Iterate through teams to find memberships +teamsList.teams.forEach { team -> + val response = teams.listMemberships(teamId = team.$id) + + // Find membership for specific user + val userMembership = response.memberships.find { + it.userId == "user_id" + } + + userMembership?.let { + println("Team: ${team.name}") + println("Joined: ${it.joined}") # null if invited, timestamp if joined + println("Roles: ${it.roles}") + } +} ``` {% /multicode %} From da60b5d828c68abfaf4f809fbbdc93e344badcf3 Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Mon, 27 Jan 2025 20:45:21 +0000 Subject: [PATCH 15/19] update to client-android-kotlin --- src/routes/docs/products/auth/team-invites/+page.markdoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/docs/products/auth/team-invites/+page.markdoc b/src/routes/docs/products/auth/team-invites/+page.markdoc index 480a839ac5..e0625934d6 100644 --- a/src/routes/docs/products/auth/team-invites/+page.markdoc +++ b/src/routes/docs/products/auth/team-invites/+page.markdoc @@ -66,7 +66,7 @@ let membership = try await teams.createMembership( email: "user@example.com" ) ``` -```kotlin +```client-android-kotlin import io.appwrite.Client import io.appwrite.services.Teams @@ -142,7 +142,7 @@ let response = try await teams.updateMembershipStatus( userId: "user_id" ) ``` -```kotlin +```client-android-kotlin import io.appwrite.Client import io.appwrite.services.Teams @@ -333,7 +333,7 @@ if let userMembership = response.memberships.first(where: { $0.userId == "user_i print(userMembership.roles) // ['developer', etc] } ``` -```kotlin +```client-android-kotlin import io.appwrite.Client import io.appwrite.services.Teams @@ -531,7 +531,7 @@ try await teams.deleteMembership( membershipId: "membership_id" ) ``` -```kotlin +```client-android-kotlin import io.appwrite.Client import io.appwrite.services.Teams From d30826048160bfbb1f40c0a48b1d153c61b4e2a0 Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Mon, 27 Jan 2025 21:09:57 +0000 Subject: [PATCH 16/19] Add permissions section --- .../products/auth/team-invites/+page.markdoc | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/src/routes/docs/products/auth/team-invites/+page.markdoc b/src/routes/docs/products/auth/team-invites/+page.markdoc index e0625934d6..d8dcbbdd17 100644 --- a/src/routes/docs/products/auth/team-invites/+page.markdoc +++ b/src/routes/docs/products/auth/team-invites/+page.markdoc @@ -548,6 +548,163 @@ teams.deleteMembership( ``` {% /multicode %} +# Manage team permissions {% #permissions %} + +Teams in Appwrite use a role-based access control (RBAC) system. Each team member can be assigned one or more roles that define their permissions within the team. + +## Update roles {% #update-roles %} + +You can assign roles when creating a membership or update them later. Note that only team members with the owner role can update other members' roles: + +{% multicode %} +```client-web +import { Client, Teams } from "appwrite" + +const client = new Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject('') + +const teams = new Teams(client) + +// Update member roles +await teams.updateMembership( + 'team_id', + 'membership_id', + ['admin', 'developer'] +) +``` +```client-flutter +import 'package:appwrite/appwrite.dart' + +final client = Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject('') + +final teams = Teams(client) + +// Update member roles +await teams.updateMembership( + teamId: 'team_id', + membershipId: 'membership_id', + roles: ['admin', 'developer'] +) +``` +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +let teams = Teams(client) + +// Update member roles +try await teams.updateMembership( + teamId: "team_id", + membershipId: "membership_id", + roles: ["admin", "developer"] +) +``` +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.services.Teams + +val client = Client(context) + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +val teams = Teams(client) + +// Update member roles +teams.updateMembership( + teamId = "team_id", + membershipId = "membership_id", + roles = listOf("admin", "developer") +) +``` +{% /multicode %} + +## Check role access {% #check-role-access %} + +You can verify if a user has specific roles: + +{% multicode %} +```client-web +import { Client, Teams } from "appwrite" + +const client = new Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject('') + +const teams = new Teams(client) + +// Get team memberships +const response = await teams.listMemberships('team_id') + +// Check if user has specific role +const membership = response.memberships.find(m => m.userId === 'user_id') +const isAdmin = membership?.roles.includes('admin') ?? false +``` +```client-flutter +import 'package:appwrite/appwrite.dart' + +final client = Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject('') + +final teams = Teams(client) + +// Get team memberships +final response = await teams.listMemberships( + teamId: 'team_id' +) + +// Check if user has specific role +final membership = response.memberships.firstWhere( + (m) => m.userId == 'user_id', + orElse: () => null +) +final isAdmin = membership?.roles.contains('admin') ?? false +``` +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +let teams = Teams(client) + +// Get team memberships +let response = try await teams.listMemberships( + teamId: "team_id" +) + +// Check if user has specific role +let membership = response.memberships.first { $0.userId == "user_id" } +let isAdmin = membership?.roles.contains("admin") ?? false +``` +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.services.Teams + +val client = Client(context) + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +val teams = Teams(client) + +// Get team memberships +val response = teams.listMemberships( + teamId = "team_id" +) + +// Check if user has specific role +val membership = response.memberships.find { it.userId == "user_id" } +val isAdmin = membership?.roles?.contains("admin") ?: false +``` +{% /multicode %} + {% arrow_link href="/docs/products/auth/teams" %} Learn more about team management {% /arrow_link %} \ No newline at end of file From b47d9ff21976c09e7c1b8543f2fc69465c1ee9a5 Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Mon, 27 Jan 2025 21:14:16 +0000 Subject: [PATCH 17/19] Update to "manage team invites" --- src/routes/docs/products/auth/+layout.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/docs/products/auth/+layout.svelte b/src/routes/docs/products/auth/+layout.svelte index 87e1faa96f..7aae4fd13d 100644 --- a/src/routes/docs/products/auth/+layout.svelte +++ b/src/routes/docs/products/auth/+layout.svelte @@ -98,7 +98,7 @@ href: '/docs/products/auth/verify-user' }, { - label: 'Team invites', + label: 'Manage team invites', href: '/docs/products/auth/team-invites' } ] From 849aa43d29f09d9444d96514923297a5aafbd321 Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Mon, 27 Jan 2025 21:31:11 +0000 Subject: [PATCH 18/19] Revert to team-invites --- src/routes/docs/products/auth/+layout.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/docs/products/auth/+layout.svelte b/src/routes/docs/products/auth/+layout.svelte index 7aae4fd13d..87e1faa96f 100644 --- a/src/routes/docs/products/auth/+layout.svelte +++ b/src/routes/docs/products/auth/+layout.svelte @@ -98,7 +98,7 @@ href: '/docs/products/auth/verify-user' }, { - label: 'Manage team invites', + label: 'Team invites', href: '/docs/products/auth/team-invites' } ] From 2450376aa12414b67018adb3326e392779490f2b Mon Sep 17 00:00:00 2001 From: Ebenezer Don Date: Mon, 27 Jan 2025 21:45:54 +0000 Subject: [PATCH 19/19] Update to "User verification" --- src/routes/docs/products/auth/+layout.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/docs/products/auth/+layout.svelte b/src/routes/docs/products/auth/+layout.svelte index 87e1faa96f..fff83c72f4 100644 --- a/src/routes/docs/products/auth/+layout.svelte +++ b/src/routes/docs/products/auth/+layout.svelte @@ -94,7 +94,7 @@ href: '/docs/products/auth/mfa' }, { - label: 'Verify user', + label: 'User verification', href: '/docs/products/auth/verify-user' }, {