Unverified Commit d688914b authored by boojack's avatar boojack Committed by GitHub

feat(auth): add SSO user identity linkage (#5883)

parent 50638040
......@@ -9,9 +9,6 @@ build/
bin/
memos
# Plan/design documents
docs/plans/
.DS_Store
# Jetbrains
......
......@@ -2,7 +2,7 @@
SSO sign-in in memos currently treats the IdP-provided identifier as the local username. The identifier value comes from the OAuth2 UserInfo claim named in `FieldMapping.identifier`, while local usernames are validated by `validateUsername` against `base.UIDMatcher`. Real IdPs frequently emit identifiers such as email addresses, opaque subject IDs, or provider-specific account IDs that are valid authentication subjects but are not valid memos usernames.
The existing issue artifacts under `docs/issues/2026-04-21-sso-user-identity-linkage/` already scope a persistent linkage between SSO identities and local users. A broader review of upstream open source schemas now shows that similar systems converge on separating external identity from the local user row, but do not converge on one universal table name or one exact column set. That difference matters because the implementation problem is narrower than "copy one upstream schema exactly" and broader than "pick any new table name locally."
The existing issue artifacts under `docs/plans/2026-04-21-sso-user-identity-linkage/` already scope a persistent linkage between SSO identities and local users. A broader review of upstream open source schemas now shows that similar systems converge on separating external identity from the local user row, but do not converge on one universal table name or one exact column set. That difference matters because the implementation problem is narrower than "copy one upstream schema exactly" and broader than "pick any new table name locally."
## Issue Statement
......
## Execution Log
### T1: Add `user_identity` migrations + LATEST.sql updates
**Status**: Completed
**Files Changed**:
- Created: `store/migration/sqlite/0.28/00__user_identity.sql`
- Created: `store/migration/postgres/0.28/00__user_identity.sql`
- Created: `store/migration/mysql/0.28/00__user_identity.sql`
- Modified: `store/migration/sqlite/LATEST.sql`
- Modified: `store/migration/postgres/LATEST.sql`
- Modified: `store/migration/mysql/LATEST.sql`
**Validation**:
- `rg 'CREATE TABLE \`?user_identity\`?' store/migration` — PASS (hits in all 6 expected files).
- `rg 'UNIQUE \(\`?provider\`?, \`?extern_uid\`?\)' store/migration` — PASS (6 hits).
- `go build ./...` — PASS.
**Path Corrections**: None.
**Deviations**: None.
### T2: Add `store.UserIdentity` model, `Store` methods, and driver interface
**Status**: Completed
**Files Changed**:
- Created: `store/user_identity.go`
- Modified: `store/driver.go`
**Validation**:
- Interface-only build is expected to fail until T3–T5; deferred compile check to T5.
- `rg 'CreateUserIdentity|ListUserIdentities' store/driver.go store/user_identity.go` — PASS (method declarations present in both files).
**Path Corrections**: None.
**Deviations**: None.
### T3: Implement SQLite driver for `user_identity`
**Status**: Completed
**Files Changed**:
- Created: `store/db/sqlite/user_identity.go`
**Validation**:
- `go build ./store/db/sqlite/...` — PASS.
**Path Corrections**: None.
**Deviations**: None.
### T4: Implement Postgres driver for `user_identity`
**Status**: Completed
**Files Changed**:
- Created: `store/db/postgres/user_identity.go`
**Validation**:
- `go build ./store/db/postgres/...` — PASS.
**Path Corrections**: None.
**Deviations**: None.
### T5: Implement MySQL driver for `user_identity`
**Status**: Completed
**Files Changed**:
- Created: `store/db/mysql/user_identity.go`
**Validation**:
- `go build ./...` — PASS (whole repo compiles; all drivers satisfy the `Driver` interface).
**Path Corrections**: None.
**Deviations**: None.
### T6: Add store-layer tests for `user_identity`
**Status**: Completed
**Files Changed**:
- Created: `store/test/user_identity_test.go`
**Validation**:
- `DRIVER=sqlite go test ./store/test/ -run TestUserIdentity -count=1 -v` — PASS:
- `TestUserIdentityCreateAndGet` — PASS
- `TestUserIdentityListByUserID` — PASS
- `TestUserIdentityUniqueConflict` — PASS
- `TestUserIdentitySameExternUIDDifferentProviders` — PASS
**Path Corrections**: None.
**Deviations**: None.
### T7: Add SSO username derivation helper
**Status**: Completed
**Files Changed**:
- Created: `server/router/api/v1/sso_username.go`
**Validation**:
- `go build ./server/router/api/v1/...` — PASS.
- `go vet ./server/router/api/v1/...` — PASS.
**Path Corrections**: None.
**Deviations**: None.
### T8: Route SSO sign-in through `user_identity` linkage
**Status**: Completed
**Files Changed**:
- Modified: `server/router/api/v1/auth_service.go`
- `SignIn` SSO branch now delegates user resolution to a new `resolveSSOUser` method.
- `resolveSSOUser` does: `user_identity` lookup → hit path (load user by linked `user_id`); miss path (registration gate → `deriveSSOUsername` → create user → create linkage → race recovery on unique(provider, extern_uid)).
- Added `isUserIdentityUniqueViolation` helper (string match on the three backends' unique-constraint error strings, matching the pattern in `memo_service.go:103–105`).
**Validation**:
- `go build ./...` — PASS.
- `go vet ./...` — PASS.
- `DRIVER=sqlite go test ./store/test/ -run TestUserIdentity -count=1` — PASS (regression check).
**Path Corrections**:
- The plan pseudocode referenced `identityProvider.UID`; the actual protobuf type `storepb.IdentityProvider` exposes the field as `Uid`. Used `identityProvider.Uid` in the implementation. No semantic deviation.
**Deviations**: None.
## Completion Declaration
**All tasks completed successfully.**
## Task List
**Task Index**
> T1: Add `user_identity` migrations + LATEST.sql updates for all three backends [M] — T2: Add `store.UserIdentity` model and `Store` methods + driver interface [M] — T3: Implement SQLite driver for `user_identity` [M] — T4: Implement Postgres driver for `user_identity` [M] — T5: Implement MySQL driver for `user_identity` [M] — T6: Add store-layer tests for `user_identity` [M] — T7: Add SSO username derivation helper [M] — T8: Route SSO sign-in through `user_identity` linkage [L]
### T1: Add `user_identity` migrations + LATEST.sql updates [M]
**Objective**: Create the `user_identity` persistence structure across SQLite, Postgres, and MySQL, and reflect it in `LATEST.sql` for fresh installs (G1, G2, G3, G4, G5; design §1, §5).
**Size**: M (3 new migration files, 3 LATEST.sql edits; straightforward DDL).
**Files**:
- Create: `store/migration/sqlite/0.28/00__user_identity.sql`
- Create: `store/migration/postgres/0.28/00__user_identity.sql`
- Create: `store/migration/mysql/0.28/00__user_identity.sql`
- Modify: `store/migration/sqlite/LATEST.sql`
- Modify: `store/migration/postgres/LATEST.sql`
- Modify: `store/migration/mysql/LATEST.sql`
**Implementation**:
1. `store/migration/sqlite/0.28/00__user_identity.sql`:
```sql
CREATE TABLE user_identity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
provider TEXT NOT NULL,
extern_uid TEXT NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE (provider, extern_uid)
);
CREATE INDEX idx_user_identity_user_id ON user_identity(user_id);
```
2. `store/migration/postgres/0.28/00__user_identity.sql`: same logical schema with Postgres types — `id SERIAL PRIMARY KEY`, `user_id INTEGER NOT NULL`, `provider TEXT NOT NULL`, `extern_uid TEXT NOT NULL`, `created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())`, `updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())`, `UNIQUE(provider, extern_uid)`, plus `CREATE INDEX idx_user_identity_user_id ON user_identity(user_id);`. Include a 2-line header comment describing the table purpose (pattern-match `04__memo_share.sql`).
3. `store/migration/mysql/0.28/00__user_identity.sql`: same logical schema with MySQL syntax — backticked identifiers, `INT NOT NULL AUTO_INCREMENT PRIMARY KEY`, `VARCHAR(256)` for `provider`, `VARCHAR(256)` for `extern_uid` (so unique key fits within index limits), `BIGINT NOT NULL DEFAULT (UNIX_TIMESTAMP())` for timestamps, `UNIQUE(provider, extern_uid)`, plus `CREATE INDEX idx_user_identity_user_id ON user_identity(user_id);`.
4. Append a `-- user_identity` section to each `LATEST.sql` mirroring the corresponding migration file (schema only, same indentation style used by neighboring tables in that file).
**Boundaries**: Must NOT alter the `user` or `idp` tables; must NOT add FK from `user_identity.provider` to `idp.uid`; must NOT add columns beyond `id`, `user_id`, `provider`, `extern_uid`, `created_ts`, `updated_ts`.
**Dependencies**: None.
**Expected Outcome**: New migration files exist; `LATEST.sql` for each backend contains a `user_identity` table block and its `user_id` index.
**Validation**:
- `rg -n "CREATE TABLE user_identity" store/migration` — expects one hit per backend in both the 0.28 migration and `LATEST.sql` (6 hits total).
- `rg -n "UNIQUE ?\\(provider, extern_uid\\)" store/migration` — expects 6 hits total.
- `go build ./...` — expects PASS (no code changes affect the build; confirms no stray syntax issues).
---
### T2: Add `store.UserIdentity` model, `Store` methods, and driver interface [M]
**Objective**: Provide a Go-level abstraction for the `user_identity` record with create/read operations wired through `store.Driver` (design §2, G3, G5).
**Size**: M (one new store file, one interface edit; simple CRUD-shaped code).
**Files**:
- Create: `store/user_identity.go`
- Modify: `store/driver.go`
**Implementation**:
1. `store/user_identity.go`:
- Types:
```go
type UserIdentity struct {
ID int32
UserID int32
Provider string
ExternUID string
CreatedTs int64
UpdatedTs int64
}
type FindUserIdentity struct {
ID *int32
UserID *int32
Provider *string
ExternUID *string
}
```
- Store methods (thin passthroughs to driver):
```go
func (s *Store) CreateUserIdentity(ctx context.Context, create *UserIdentity) (*UserIdentity, error)
func (s *Store) ListUserIdentities(ctx context.Context, find *FindUserIdentity) ([]*UserIdentity, error)
func (s *Store) GetUserIdentity(ctx context.Context, find *FindUserIdentity) (*UserIdentity, error) // returns (nil, nil) on no match
```
- No update/delete methods in this issue (design §2: create/read only).
2. `store/driver.go`: extend the `Driver` interface with:
```go
// UserIdentity model related methods.
CreateUserIdentity(ctx context.Context, create *UserIdentity) (*UserIdentity, error)
ListUserIdentities(ctx context.Context, find *FindUserIdentity) ([]*UserIdentity, error)
```
`GetUserIdentity` in `store` can be implemented locally by calling `ListUserIdentities` with `Limit`-free semantics and returning the first row, matching the `GetMemoShare`/`GetIdentityProvider` pattern (no new driver method required for "get").
**Boundaries**: Must NOT add fields to `store.User` or `store.UpdateUser`; must NOT add update/delete methods.
**Dependencies**: None (T3–T5 will satisfy the new interface methods).
**Expected Outcome**: `store.UserIdentity`, `FindUserIdentity`, and three `Store` methods exist; `Driver` interface declares the two new methods.
**Validation**:
- `go build ./store/...` — expects FAIL until T3–T5 implement the interface on each driver. Record as expected; final pass comes at end of T5.
- `rg -n "CreateUserIdentity|ListUserIdentities" store/driver.go store/user_identity.go` — expects method declarations in both files.
---
### T3: Implement SQLite driver for `user_identity` [M]
**Objective**: Implement `CreateUserIdentity` and `ListUserIdentities` for SQLite so the interface declared in T2 is satisfied (design §2).
**Size**: M (one new driver file; mirrors existing `memo_share.go` patterns).
**Files**:
- Create: `store/db/sqlite/user_identity.go`
**Implementation**:
1. `CreateUserIdentity`:
- Insert columns `user_id`, `provider`, `extern_uid` using `?` placeholders.
- Use `RETURNING id, created_ts, updated_ts` to populate generated fields, same pattern as `store/db/sqlite/memo_share.go:24`.
- Return the passed-in `create` struct with generated fields populated, or the error from `QueryRowContext(...).Scan(...)` (unique-constraint violation surfaces to caller unchanged).
2. `ListUserIdentities`:
- `where := []string{"1 = 1"}`; append clauses for `find.ID`, `find.UserID`, `find.Provider`, `find.ExternUID` when non-nil.
- `SELECT id, user_id, provider, extern_uid, created_ts, updated_ts FROM user_identity WHERE ... ORDER BY id ASC`.
- Scan rows into `[]*store.UserIdentity`; return `[]*store.UserIdentity{}` on no rows (not nil).
**Boundaries**: Must NOT introduce transaction helpers, upsert semantics, or extra scan columns.
**Dependencies**: T2.
**Expected Outcome**: SQLite driver compiles and returns populated rows.
**Validation**:
- `go build ./store/db/sqlite/...` — expects PASS.
---
### T4: Implement Postgres driver for `user_identity` [M]
**Objective**: Mirror T3 for Postgres using `$N` placeholders and `SERIAL` semantics (design §2).
**Size**: M (one new driver file; mirrors `store/db/postgres/memo_share.go`).
**Files**:
- Create: `store/db/postgres/user_identity.go`
**Implementation**:
- Same shape as T3, but:
- Use `placeholder(n)` / `placeholders(n)` helpers from `store/db/postgres/common.go`.
- Insert stmt `INSERT INTO user_identity (user_id, provider, extern_uid) VALUES (...) RETURNING id, created_ts, updated_ts`.
- List query identical SQL shape to SQLite (no backticks in Postgres; match `memo_share.go` style).
**Boundaries**: Same as T3.
**Dependencies**: T2.
**Expected Outcome**: Postgres driver compiles.
**Validation**:
- `go build ./store/db/postgres/...` — expects PASS.
---
### T5: Implement MySQL driver for `user_identity` [M]
**Objective**: Mirror T3/T4 for MySQL, using `LastInsertId()` + re-read pattern (MySQL's driver does not support `RETURNING`; design §2).
**Size**: M (one new driver file; mirrors `store/db/mysql/memo_share.go`).
**Files**:
- Create: `store/db/mysql/user_identity.go`
**Implementation**:
- `CreateUserIdentity`:
- `INSERT INTO user_identity (user_id, provider, extern_uid) VALUES (?, ?, ?)` via `ExecContext`.
- Get `LastInsertId()`, re-fetch via `GetUserIdentity(... ID: &id)` helper (internal unexported `listUserIdentitiesByID` or reuse `ListUserIdentities` with `FindUserIdentity{ID: &id}` + take first result).
- Mirror `memo_share.go` error-handling style (return `errors.Errorf("failed to create user identity")` when re-fetch returns nil, like memo_share does).
- `ListUserIdentities`:
- Same shape as T3, using backticked column names (`` `user_id` ``, `` `provider` ``, `` `extern_uid` ``) and `?` placeholders, matching the MySQL idiom used in `memo_share.go`.
**Boundaries**: Same as T3.
**Dependencies**: T2.
**Expected Outcome**: MySQL driver compiles; full repo builds.
**Validation**:
- `go build ./...` — expects PASS (entire repo compiles with all drivers satisfying the `Driver` interface introduced in T2).
---
### T6: Add store-layer tests for `user_identity` [M]
**Objective**: Exercise create + read paths plus the `(provider, extern_uid)` uniqueness guard across the active driver (G2).
**Size**: M (one new test file; patterns match existing store tests).
**Files**:
- Create: `store/test/user_identity_test.go`
**Implementation**:
1. `TestUserIdentityCreateAndGet`:
- Create host user via `createTestingHostUser`.
- `CreateUserIdentity` with `UserID=user.ID`, `Provider="idp-uid-1"`, `ExternUID="jane@example.com"`.
- `GetUserIdentity` by `(Provider, ExternUID)` — assert match on `UserID`, `Provider`, `ExternUID`, non-zero `ID`, non-zero `CreatedTs`.
2. `TestUserIdentityListByUserID`:
- Create two identities under the same `UserID` with two different `Provider` values.
- `ListUserIdentities` by `UserID` — assert length 2.
3. `TestUserIdentityUniqueConflict`:
- Insert one row with `(Provider="idp-A", ExternUID="sub-1")`.
- Insert a second row with identical `(Provider, ExternUID)` for a different `UserID`.
- Assert the second `CreateUserIdentity` returns a non-nil error (detection via `err != nil`; do not assert message since error strings differ per backend).
4. `TestUserIdentitySameExternUIDDifferentProviders`:
- Insert `(Provider="idp-A", ExternUID="sub-1")` and `(Provider="idp-B", ExternUID="sub-1")` under the same or different users.
- Assert both inserts succeed (G2: uniqueness is scoped to the pair, not `extern_uid` alone).
**Boundaries**: Must NOT test SSO sign-in or auth service behavior; must NOT test migration contents beyond what `NewTestingStore` already executes.
**Dependencies**: T1–T5.
**Expected Outcome**: All four tests pass against SQLite.
**Validation**:
- `go test ./store/test/ -run TestUserIdentity -count=1` — expects all 4 tests PASS.
---
### T7: Add SSO username derivation helper [M]
**Objective**: Produce a valid `User.Username` for new SSO-created users from profile fields, independent of `extern_uid` (design §4).
**Size**: M (one new file with helper + small unit test; self-contained logic).
**Files**:
- Create: `server/router/api/v1/sso_username.go`
**Implementation**:
1. `deriveSSOUsername(ctx context.Context, stores *store.Store, userInfo *idp.IdentityProviderUserInfo) (string, error)`:
- Build ordered candidate list: `[userInfo.DisplayName, userInfo.Email, userInfo.Identifier]`, skipping empty values.
- For each candidate:
1. `base := normalizeToUsername(candidate)`
2. If `validateUsername(base) == nil`:
- If no existing user with `Username=base` (via `stores.GetUser(&FindUser{Username: &base})`), return `base`.
- Else: try up to N=8 suffix retries `base + "-" + randomSuffix(6)`, where the trimmed base ensures total length ≤ 36. If a candidate passes `validateUsername` and is unique, return it.
3. If all candidates are exhausted: fall back to a purely random username `"user-" + randomSuffix(10)` validated via `validateUsername`; retry up to 5 times before returning an error.
4. `normalizeToUsername(s string) string`:
- ASCII-fold / lowercase.
- Replace every character not in `[a-zA-Z0-9]` with `-`.
- Collapse consecutive `-` into one `-`.
- Trim leading/trailing `-`.
- Truncate to 36 chars, then re-trim trailing `-` so the string still ends in alphanumeric.
- Return `""` if the result is empty or fully numeric (so the caller falls through to the next candidate).
5. Use `internal/util.RandomString` for the random suffix (already imported by `auth_service.go`).
**Boundaries**: Must NOT modify `validateUsername` or `base.UIDMatcher`; must NOT write to `user_identity` or `user` directly; must NOT call `CreateUser`.
**Dependencies**: None.
**Expected Outcome**: New file `server/router/api/v1/sso_username.go` containing the exported-for-package helper `deriveSSOUsername` and internal `normalizeToUsername`.
**Validation**:
- `go build ./server/router/api/v1/...` — expects PASS.
- `go vet ./server/router/api/v1/...` — expects PASS.
---
### T8: Route SSO sign-in through `user_identity` linkage [L]
**Objective**: Replace the `FindUser{Username: &userInfo.Identifier}` lookup and `Username: userInfo.Identifier` user creation with `user_identity`-backed lookup and derived-username user creation, satisfying G1 and G2 end-to-end (design §3).
**Size**: L (non-trivial branching logic: lookup, miss path, registration gate, race recovery).
**Files**:
- Modify: `server/router/api/v1/auth_service.go`
**Implementation** (in `SignIn`, SSO branch, replacing current lines ~124–173):
1. After `identifier_filter` check succeeds (existing `lines 124-133` unchanged), resolve the linkage:
```go
provider := identityProvider.Uid
externUID := userInfo.Identifier
existingIdentity, err := s.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
Provider: &provider,
ExternUID: &externUID,
})
// error handling → codes.Internal
```
2. **Hit path**: if `existingIdentity != nil`, load `s.Store.GetUser(ctx, &store.FindUser{ID: &existingIdentity.UserID})`; set `existingUser`; skip creation.
3. **Miss path**: gate on `instanceGeneralSetting.DisallowUserRegistration` (reuse existing flow at current lines 143–149), then:
1. `username, err := deriveSSOUsername(ctx, s.Store, userInfo)` — from T7. `codes.Internal` on error.
2. Generate random password + bcrypt hash (unchanged from current lines 160–168).
3. `user, err := s.Store.CreateUser(ctx, &store.User{Username: username, Role: store.RoleUser, Nickname: userInfo.DisplayName, Email: userInfo.Email, AvatarURL: userInfo.AvatarURL, PasswordHash: string(passwordHash)})`.
4. `_, err := s.Store.CreateUserIdentity(ctx, &store.UserIdentity{UserID: user.ID, Provider: provider, ExternUID: externUID})`.
5. **Race recovery**: if `CreateUserIdentity` returns an error whose message matches one of the known unique-constraint markers (`strings.Contains(err.Error(), "UNIQUE constraint failed")`, `"duplicate key"`, `"Duplicate entry")` — reusing the same pattern as `server/router/api/v1/memo_service.go:103–105`):
- `_ = s.Store.DeleteUser(ctx, &store.DeleteUser{ID: user.ID})` (best-effort cleanup of the provisional local user).
- Re-read the winning `user_identity` via `s.Store.GetUserIdentity(ctx, &FindUserIdentity{Provider: &provider, ExternUID: &externUID})`; if still nil, return `codes.Internal` (should not happen under correct semantics).
- Load its user via `s.Store.GetUser(ctx, &FindUser{ID: &winner.UserID})`; set `existingUser`.
6. On any other `CreateUserIdentity` error: best-effort `DeleteUser` cleanup, then return `codes.Internal`.
7. On full success: set `existingUser = user`.
4. Leave the remainder of `SignIn` (row-status check, `doSignIn`, response construction) untouched.
**Boundaries**: Must NOT touch the password-credentials branch; must NOT modify `identifier_filter` logic; must NOT touch `doSignIn`, `SignOut`, or `RefreshToken`; must NOT add new fields to `SignInRequest`/`SignInResponse`.
**Dependencies**: T2, T3, T6 minimum for SQLite confidence; T7 for the derivation helper.
**Expected Outcome**:
- Sign-in with an IdP-issued identifier that fails `base.UIDMatcher` (e.g., `jane@example.com`) succeeds: a `user_identity` row is created, and the local `User.Username` is a derived valid username.
- Repeat sign-in for the same `(provider, extern_uid)` pair loads the same user by linkage, not by username.
- Two IdPs emitting the same `extern_uid` can each link to their own local users without colliding (G2).
**Validation**:
- `go build ./...` — expects PASS.
- `go vet ./...` — expects PASS.
- `go test ./store/test/ -run TestUserIdentity -count=1` — expects PASS (T6 regression check; ensures no store-layer drift).
## Out-of-Scope Tasks
The following are explicitly deferred per `definition.md` / `design.md` and will NOT be attempted during this execution:
- UI or API surfaces for linking/unlinking external identities.
- Update or delete paths for `user_identity` rows.
- Backfill / migration of existing users whose current `Username` matches an IdP identifier.
- Non-OAUTH2 IdP types.
- Protobuf or API changes to `SignInRequest`/`SignInResponse`.
- Adding foreign keys between `user_identity.provider` and `idp.uid`.
- Running PostgreSQL or MySQL integration tests locally (validation commands only cover SQLite, which is the default `DRIVER` in `store/test/store.go`).
......@@ -90,6 +90,33 @@ service UserService {
option (google.api.method_signature) = "parent";
}
// ListLinkedIdentities returns a list of linked SSO identities for a user.
rpc ListLinkedIdentities(ListLinkedIdentitiesRequest) returns (ListLinkedIdentitiesResponse) {
option (google.api.http) = {get: "/api/v1/{parent=users/*}/linkedIdentities"};
option (google.api.method_signature) = "parent";
}
// CreateLinkedIdentity links an SSO identity to the authenticated user.
rpc CreateLinkedIdentity(CreateLinkedIdentityRequest) returns (LinkedIdentity) {
option (google.api.http) = {
post: "/api/v1/{parent=users/*}/linkedIdentities"
body: "*"
};
option (google.api.method_signature) = "parent,idp_name";
}
// GetLinkedIdentity gets a linked SSO identity for a user.
rpc GetLinkedIdentity(GetLinkedIdentityRequest) returns (LinkedIdentity) {
option (google.api.http) = {get: "/api/v1/{name=users/*/linkedIdentities/*}"};
option (google.api.method_signature) = "name";
}
// DeleteLinkedIdentity unlinks an SSO identity from a user.
rpc DeleteLinkedIdentity(DeleteLinkedIdentityRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {delete: "/api/v1/{name=users/*/linkedIdentities/*}"};
option (google.api.method_signature) = "name";
}
// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
rpc ListPersonalAccessTokens(ListPersonalAccessTokensRequest) returns (ListPersonalAccessTokensResponse) {
......@@ -466,6 +493,87 @@ message ListUserSettingsResponse {
int32 total_size = 3;
}
// LinkedIdentity represents an SSO identity linked to a user account.
message LinkedIdentity {
option (google.api.resource) = {
type: "memos.api.v1/LinkedIdentity"
pattern: "users/{user}/linkedIdentities/{linked_identity}"
singular: "linkedIdentity"
plural: "linkedIdentities"
};
// The resource name of the linked identity.
// Format: users/{user}/linkedIdentities/{linked_identity}
string name = 1 [(google.api.field_behavior) = IDENTIFIER];
// The resource name of the identity provider.
// Format: identity-providers/{uid}
string idp_name = 2 [
(google.api.field_behavior) = OUTPUT_ONLY,
(google.api.resource_reference) = {type: "memos.api.v1/IdentityProvider"}
];
// The external user identifier from the identity provider.
string extern_uid = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
}
message ListLinkedIdentitiesRequest {
// Required. The parent resource whose linked identities will be listed.
// Format: users/{user}
string parent = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/User"}
];
}
message ListLinkedIdentitiesResponse {
// The list of linked identities.
repeated LinkedIdentity linked_identities = 1;
}
message CreateLinkedIdentityRequest {
// Required. The parent user who owns the linked identity.
// Format: users/{user}
string parent = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/User"}
];
// Required. The identity provider to link.
// Format: identity-providers/{uid}
string idp_name = 2 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/IdentityProvider"}
];
// Required. The authorization code from the identity provider.
string code = 3 [(google.api.field_behavior) = REQUIRED];
// Required. The redirect URI used in the OAuth flow.
string redirect_uri = 4 [(google.api.field_behavior) = REQUIRED];
// Optional. The PKCE code verifier used in the OAuth flow.
string code_verifier = 5 [(google.api.field_behavior) = OPTIONAL];
}
message GetLinkedIdentityRequest {
// Required. The resource name of the linked identity to get.
// Format: users/{user}/linkedIdentities/{linked_identity}
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/LinkedIdentity"}
];
}
message DeleteLinkedIdentityRequest {
// Required. The resource name of the linked identity to delete.
// Format: users/{user}/linkedIdentities/{linked_identity}
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/LinkedIdentity"}
];
}
// PersonalAccessToken represents a long-lived token for API/script access.
// PATs are distinct from short-lived JWT access tokens used for session authentication.
message PersonalAccessToken {
......
......@@ -62,6 +62,18 @@ const (
// UserServiceListUserSettingsProcedure is the fully-qualified name of the UserService's
// ListUserSettings RPC.
UserServiceListUserSettingsProcedure = "/memos.api.v1.UserService/ListUserSettings"
// UserServiceListLinkedIdentitiesProcedure is the fully-qualified name of the UserService's
// ListLinkedIdentities RPC.
UserServiceListLinkedIdentitiesProcedure = "/memos.api.v1.UserService/ListLinkedIdentities"
// UserServiceCreateLinkedIdentityProcedure is the fully-qualified name of the UserService's
// CreateLinkedIdentity RPC.
UserServiceCreateLinkedIdentityProcedure = "/memos.api.v1.UserService/CreateLinkedIdentity"
// UserServiceGetLinkedIdentityProcedure is the fully-qualified name of the UserService's
// GetLinkedIdentity RPC.
UserServiceGetLinkedIdentityProcedure = "/memos.api.v1.UserService/GetLinkedIdentity"
// UserServiceDeleteLinkedIdentityProcedure is the fully-qualified name of the UserService's
// DeleteLinkedIdentity RPC.
UserServiceDeleteLinkedIdentityProcedure = "/memos.api.v1.UserService/DeleteLinkedIdentity"
// UserServiceListPersonalAccessTokensProcedure is the fully-qualified name of the UserService's
// ListPersonalAccessTokens RPC.
UserServiceListPersonalAccessTokensProcedure = "/memos.api.v1.UserService/ListPersonalAccessTokens"
......@@ -119,6 +131,14 @@ type UserServiceClient interface {
UpdateUserSetting(context.Context, *connect.Request[v1.UpdateUserSettingRequest]) (*connect.Response[v1.UserSetting], error)
// ListUserSettings returns a list of user settings.
ListUserSettings(context.Context, *connect.Request[v1.ListUserSettingsRequest]) (*connect.Response[v1.ListUserSettingsResponse], error)
// ListLinkedIdentities returns a list of linked SSO identities for a user.
ListLinkedIdentities(context.Context, *connect.Request[v1.ListLinkedIdentitiesRequest]) (*connect.Response[v1.ListLinkedIdentitiesResponse], error)
// CreateLinkedIdentity links an SSO identity to the authenticated user.
CreateLinkedIdentity(context.Context, *connect.Request[v1.CreateLinkedIdentityRequest]) (*connect.Response[v1.LinkedIdentity], error)
// GetLinkedIdentity gets a linked SSO identity for a user.
GetLinkedIdentity(context.Context, *connect.Request[v1.GetLinkedIdentityRequest]) (*connect.Response[v1.LinkedIdentity], error)
// DeleteLinkedIdentity unlinks an SSO identity from a user.
DeleteLinkedIdentity(context.Context, *connect.Request[v1.DeleteLinkedIdentityRequest]) (*connect.Response[emptypb.Empty], error)
// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
ListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error)
......@@ -220,6 +240,30 @@ func NewUserServiceClient(httpClient connect.HTTPClient, baseURL string, opts ..
connect.WithSchema(userServiceMethods.ByName("ListUserSettings")),
connect.WithClientOptions(opts...),
),
listLinkedIdentities: connect.NewClient[v1.ListLinkedIdentitiesRequest, v1.ListLinkedIdentitiesResponse](
httpClient,
baseURL+UserServiceListLinkedIdentitiesProcedure,
connect.WithSchema(userServiceMethods.ByName("ListLinkedIdentities")),
connect.WithClientOptions(opts...),
),
createLinkedIdentity: connect.NewClient[v1.CreateLinkedIdentityRequest, v1.LinkedIdentity](
httpClient,
baseURL+UserServiceCreateLinkedIdentityProcedure,
connect.WithSchema(userServiceMethods.ByName("CreateLinkedIdentity")),
connect.WithClientOptions(opts...),
),
getLinkedIdentity: connect.NewClient[v1.GetLinkedIdentityRequest, v1.LinkedIdentity](
httpClient,
baseURL+UserServiceGetLinkedIdentityProcedure,
connect.WithSchema(userServiceMethods.ByName("GetLinkedIdentity")),
connect.WithClientOptions(opts...),
),
deleteLinkedIdentity: connect.NewClient[v1.DeleteLinkedIdentityRequest, emptypb.Empty](
httpClient,
baseURL+UserServiceDeleteLinkedIdentityProcedure,
connect.WithSchema(userServiceMethods.ByName("DeleteLinkedIdentity")),
connect.WithClientOptions(opts...),
),
listPersonalAccessTokens: connect.NewClient[v1.ListPersonalAccessTokensRequest, v1.ListPersonalAccessTokensResponse](
httpClient,
baseURL+UserServiceListPersonalAccessTokensProcedure,
......@@ -296,6 +340,10 @@ type userServiceClient struct {
getUserSetting *connect.Client[v1.GetUserSettingRequest, v1.UserSetting]
updateUserSetting *connect.Client[v1.UpdateUserSettingRequest, v1.UserSetting]
listUserSettings *connect.Client[v1.ListUserSettingsRequest, v1.ListUserSettingsResponse]
listLinkedIdentities *connect.Client[v1.ListLinkedIdentitiesRequest, v1.ListLinkedIdentitiesResponse]
createLinkedIdentity *connect.Client[v1.CreateLinkedIdentityRequest, v1.LinkedIdentity]
getLinkedIdentity *connect.Client[v1.GetLinkedIdentityRequest, v1.LinkedIdentity]
deleteLinkedIdentity *connect.Client[v1.DeleteLinkedIdentityRequest, emptypb.Empty]
listPersonalAccessTokens *connect.Client[v1.ListPersonalAccessTokensRequest, v1.ListPersonalAccessTokensResponse]
createPersonalAccessToken *connect.Client[v1.CreatePersonalAccessTokenRequest, v1.CreatePersonalAccessTokenResponse]
deletePersonalAccessToken *connect.Client[v1.DeletePersonalAccessTokenRequest, emptypb.Empty]
......@@ -363,6 +411,26 @@ func (c *userServiceClient) ListUserSettings(ctx context.Context, req *connect.R
return c.listUserSettings.CallUnary(ctx, req)
}
// ListLinkedIdentities calls memos.api.v1.UserService.ListLinkedIdentities.
func (c *userServiceClient) ListLinkedIdentities(ctx context.Context, req *connect.Request[v1.ListLinkedIdentitiesRequest]) (*connect.Response[v1.ListLinkedIdentitiesResponse], error) {
return c.listLinkedIdentities.CallUnary(ctx, req)
}
// CreateLinkedIdentity calls memos.api.v1.UserService.CreateLinkedIdentity.
func (c *userServiceClient) CreateLinkedIdentity(ctx context.Context, req *connect.Request[v1.CreateLinkedIdentityRequest]) (*connect.Response[v1.LinkedIdentity], error) {
return c.createLinkedIdentity.CallUnary(ctx, req)
}
// GetLinkedIdentity calls memos.api.v1.UserService.GetLinkedIdentity.
func (c *userServiceClient) GetLinkedIdentity(ctx context.Context, req *connect.Request[v1.GetLinkedIdentityRequest]) (*connect.Response[v1.LinkedIdentity], error) {
return c.getLinkedIdentity.CallUnary(ctx, req)
}
// DeleteLinkedIdentity calls memos.api.v1.UserService.DeleteLinkedIdentity.
func (c *userServiceClient) DeleteLinkedIdentity(ctx context.Context, req *connect.Request[v1.DeleteLinkedIdentityRequest]) (*connect.Response[emptypb.Empty], error) {
return c.deleteLinkedIdentity.CallUnary(ctx, req)
}
// ListPersonalAccessTokens calls memos.api.v1.UserService.ListPersonalAccessTokens.
func (c *userServiceClient) ListPersonalAccessTokens(ctx context.Context, req *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error) {
return c.listPersonalAccessTokens.CallUnary(ctx, req)
......@@ -438,6 +506,14 @@ type UserServiceHandler interface {
UpdateUserSetting(context.Context, *connect.Request[v1.UpdateUserSettingRequest]) (*connect.Response[v1.UserSetting], error)
// ListUserSettings returns a list of user settings.
ListUserSettings(context.Context, *connect.Request[v1.ListUserSettingsRequest]) (*connect.Response[v1.ListUserSettingsResponse], error)
// ListLinkedIdentities returns a list of linked SSO identities for a user.
ListLinkedIdentities(context.Context, *connect.Request[v1.ListLinkedIdentitiesRequest]) (*connect.Response[v1.ListLinkedIdentitiesResponse], error)
// CreateLinkedIdentity links an SSO identity to the authenticated user.
CreateLinkedIdentity(context.Context, *connect.Request[v1.CreateLinkedIdentityRequest]) (*connect.Response[v1.LinkedIdentity], error)
// GetLinkedIdentity gets a linked SSO identity for a user.
GetLinkedIdentity(context.Context, *connect.Request[v1.GetLinkedIdentityRequest]) (*connect.Response[v1.LinkedIdentity], error)
// DeleteLinkedIdentity unlinks an SSO identity from a user.
DeleteLinkedIdentity(context.Context, *connect.Request[v1.DeleteLinkedIdentityRequest]) (*connect.Response[emptypb.Empty], error)
// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
ListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error)
......@@ -535,6 +611,30 @@ func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption
connect.WithSchema(userServiceMethods.ByName("ListUserSettings")),
connect.WithHandlerOptions(opts...),
)
userServiceListLinkedIdentitiesHandler := connect.NewUnaryHandler(
UserServiceListLinkedIdentitiesProcedure,
svc.ListLinkedIdentities,
connect.WithSchema(userServiceMethods.ByName("ListLinkedIdentities")),
connect.WithHandlerOptions(opts...),
)
userServiceCreateLinkedIdentityHandler := connect.NewUnaryHandler(
UserServiceCreateLinkedIdentityProcedure,
svc.CreateLinkedIdentity,
connect.WithSchema(userServiceMethods.ByName("CreateLinkedIdentity")),
connect.WithHandlerOptions(opts...),
)
userServiceGetLinkedIdentityHandler := connect.NewUnaryHandler(
UserServiceGetLinkedIdentityProcedure,
svc.GetLinkedIdentity,
connect.WithSchema(userServiceMethods.ByName("GetLinkedIdentity")),
connect.WithHandlerOptions(opts...),
)
userServiceDeleteLinkedIdentityHandler := connect.NewUnaryHandler(
UserServiceDeleteLinkedIdentityProcedure,
svc.DeleteLinkedIdentity,
connect.WithSchema(userServiceMethods.ByName("DeleteLinkedIdentity")),
connect.WithHandlerOptions(opts...),
)
userServiceListPersonalAccessTokensHandler := connect.NewUnaryHandler(
UserServiceListPersonalAccessTokensProcedure,
svc.ListPersonalAccessTokens,
......@@ -619,6 +719,14 @@ func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption
userServiceUpdateUserSettingHandler.ServeHTTP(w, r)
case UserServiceListUserSettingsProcedure:
userServiceListUserSettingsHandler.ServeHTTP(w, r)
case UserServiceListLinkedIdentitiesProcedure:
userServiceListLinkedIdentitiesHandler.ServeHTTP(w, r)
case UserServiceCreateLinkedIdentityProcedure:
userServiceCreateLinkedIdentityHandler.ServeHTTP(w, r)
case UserServiceGetLinkedIdentityProcedure:
userServiceGetLinkedIdentityHandler.ServeHTTP(w, r)
case UserServiceDeleteLinkedIdentityProcedure:
userServiceDeleteLinkedIdentityHandler.ServeHTTP(w, r)
case UserServiceListPersonalAccessTokensProcedure:
userServiceListPersonalAccessTokensHandler.ServeHTTP(w, r)
case UserServiceCreatePersonalAccessTokenProcedure:
......@@ -692,6 +800,22 @@ func (UnimplementedUserServiceHandler) ListUserSettings(context.Context, *connec
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListUserSettings is not implemented"))
}
func (UnimplementedUserServiceHandler) ListLinkedIdentities(context.Context, *connect.Request[v1.ListLinkedIdentitiesRequest]) (*connect.Response[v1.ListLinkedIdentitiesResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListLinkedIdentities is not implemented"))
}
func (UnimplementedUserServiceHandler) CreateLinkedIdentity(context.Context, *connect.Request[v1.CreateLinkedIdentityRequest]) (*connect.Response[v1.LinkedIdentity], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.CreateLinkedIdentity is not implemented"))
}
func (UnimplementedUserServiceHandler) GetLinkedIdentity(context.Context, *connect.Request[v1.GetLinkedIdentityRequest]) (*connect.Response[v1.LinkedIdentity], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.GetLinkedIdentity is not implemented"))
}
func (UnimplementedUserServiceHandler) DeleteLinkedIdentity(context.Context, *connect.Request[v1.DeleteLinkedIdentityRequest]) (*connect.Response[emptypb.Empty], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.DeleteLinkedIdentity is not implemented"))
}
func (UnimplementedUserServiceHandler) ListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListPersonalAccessTokens is not implemented"))
}
......
......@@ -175,7 +175,7 @@ func (x UserNotification_Status) Number() protoreflect.EnumNumber {
// Deprecated: Use UserNotification_Status.Descriptor instead.
func (UserNotification_Status) EnumDescriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{30, 0}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{36, 0}
}
type UserNotification_Type int32
......@@ -224,7 +224,7 @@ func (x UserNotification_Type) Number() protoreflect.EnumNumber {
// Deprecated: Use UserNotification_Type.Descriptor instead.
func (UserNotification_Type) EnumDescriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{30, 1}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{36, 1}
}
type User struct {
......@@ -1390,6 +1390,338 @@ func (x *ListUserSettingsResponse) GetTotalSize() int32 {
return 0
}
// LinkedIdentity represents an SSO identity linked to a user account.
type LinkedIdentity struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The resource name of the linked identity.
// Format: users/{user}/linkedIdentities/{linked_identity}
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
// The resource name of the identity provider.
// Format: identity-providers/{uid}
IdpName string `protobuf:"bytes,2,opt,name=idp_name,json=idpName,proto3" json:"idp_name,omitempty"`
// The external user identifier from the identity provider.
ExternUid string `protobuf:"bytes,3,opt,name=extern_uid,json=externUid,proto3" json:"extern_uid,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LinkedIdentity) Reset() {
*x = LinkedIdentity{}
mi := &file_api_v1_user_service_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LinkedIdentity) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LinkedIdentity) ProtoMessage() {}
func (x *LinkedIdentity) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LinkedIdentity.ProtoReflect.Descriptor instead.
func (*LinkedIdentity) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{18}
}
func (x *LinkedIdentity) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *LinkedIdentity) GetIdpName() string {
if x != nil {
return x.IdpName
}
return ""
}
func (x *LinkedIdentity) GetExternUid() string {
if x != nil {
return x.ExternUid
}
return ""
}
type ListLinkedIdentitiesRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Required. The parent resource whose linked identities will be listed.
// Format: users/{user}
Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListLinkedIdentitiesRequest) Reset() {
*x = ListLinkedIdentitiesRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListLinkedIdentitiesRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListLinkedIdentitiesRequest) ProtoMessage() {}
func (x *ListLinkedIdentitiesRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListLinkedIdentitiesRequest.ProtoReflect.Descriptor instead.
func (*ListLinkedIdentitiesRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{19}
}
func (x *ListLinkedIdentitiesRequest) GetParent() string {
if x != nil {
return x.Parent
}
return ""
}
type ListLinkedIdentitiesResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The list of linked identities.
LinkedIdentities []*LinkedIdentity `protobuf:"bytes,1,rep,name=linked_identities,json=linkedIdentities,proto3" json:"linked_identities,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListLinkedIdentitiesResponse) Reset() {
*x = ListLinkedIdentitiesResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListLinkedIdentitiesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListLinkedIdentitiesResponse) ProtoMessage() {}
func (x *ListLinkedIdentitiesResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListLinkedIdentitiesResponse.ProtoReflect.Descriptor instead.
func (*ListLinkedIdentitiesResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{20}
}
func (x *ListLinkedIdentitiesResponse) GetLinkedIdentities() []*LinkedIdentity {
if x != nil {
return x.LinkedIdentities
}
return nil
}
type CreateLinkedIdentityRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Required. The parent user who owns the linked identity.
// Format: users/{user}
Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"`
// Required. The identity provider to link.
// Format: identity-providers/{uid}
IdpName string `protobuf:"bytes,2,opt,name=idp_name,json=idpName,proto3" json:"idp_name,omitempty"`
// Required. The authorization code from the identity provider.
Code string `protobuf:"bytes,3,opt,name=code,proto3" json:"code,omitempty"`
// Required. The redirect URI used in the OAuth flow.
RedirectUri string `protobuf:"bytes,4,opt,name=redirect_uri,json=redirectUri,proto3" json:"redirect_uri,omitempty"`
// Optional. The PKCE code verifier used in the OAuth flow.
CodeVerifier string `protobuf:"bytes,5,opt,name=code_verifier,json=codeVerifier,proto3" json:"code_verifier,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateLinkedIdentityRequest) Reset() {
*x = CreateLinkedIdentityRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateLinkedIdentityRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateLinkedIdentityRequest) ProtoMessage() {}
func (x *CreateLinkedIdentityRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateLinkedIdentityRequest.ProtoReflect.Descriptor instead.
func (*CreateLinkedIdentityRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{21}
}
func (x *CreateLinkedIdentityRequest) GetParent() string {
if x != nil {
return x.Parent
}
return ""
}
func (x *CreateLinkedIdentityRequest) GetIdpName() string {
if x != nil {
return x.IdpName
}
return ""
}
func (x *CreateLinkedIdentityRequest) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
func (x *CreateLinkedIdentityRequest) GetRedirectUri() string {
if x != nil {
return x.RedirectUri
}
return ""
}
func (x *CreateLinkedIdentityRequest) GetCodeVerifier() string {
if x != nil {
return x.CodeVerifier
}
return ""
}
type GetLinkedIdentityRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Required. The resource name of the linked identity to get.
// Format: users/{user}/linkedIdentities/{linked_identity}
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetLinkedIdentityRequest) Reset() {
*x = GetLinkedIdentityRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetLinkedIdentityRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetLinkedIdentityRequest) ProtoMessage() {}
func (x *GetLinkedIdentityRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetLinkedIdentityRequest.ProtoReflect.Descriptor instead.
func (*GetLinkedIdentityRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{22}
}
func (x *GetLinkedIdentityRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type DeleteLinkedIdentityRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Required. The resource name of the linked identity to delete.
// Format: users/{user}/linkedIdentities/{linked_identity}
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteLinkedIdentityRequest) Reset() {
*x = DeleteLinkedIdentityRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteLinkedIdentityRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteLinkedIdentityRequest) ProtoMessage() {}
func (x *DeleteLinkedIdentityRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteLinkedIdentityRequest.ProtoReflect.Descriptor instead.
func (*DeleteLinkedIdentityRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{23}
}
func (x *DeleteLinkedIdentityRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
// PersonalAccessToken represents a long-lived token for API/script access.
// PATs are distinct from short-lived JWT access tokens used for session authentication.
type PersonalAccessToken struct {
......@@ -1411,7 +1743,7 @@ type PersonalAccessToken struct {
func (x *PersonalAccessToken) Reset() {
*x = PersonalAccessToken{}
mi := &file_api_v1_user_service_proto_msgTypes[18]
mi := &file_api_v1_user_service_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1423,7 +1755,7 @@ func (x *PersonalAccessToken) String() string {
func (*PersonalAccessToken) ProtoMessage() {}
func (x *PersonalAccessToken) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[18]
mi := &file_api_v1_user_service_proto_msgTypes[24]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1436,7 +1768,7 @@ func (x *PersonalAccessToken) ProtoReflect() protoreflect.Message {
// Deprecated: Use PersonalAccessToken.ProtoReflect.Descriptor instead.
func (*PersonalAccessToken) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{18}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{24}
}
func (x *PersonalAccessToken) GetName() string {
......@@ -1489,7 +1821,7 @@ type ListPersonalAccessTokensRequest struct {
func (x *ListPersonalAccessTokensRequest) Reset() {
*x = ListPersonalAccessTokensRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[19]
mi := &file_api_v1_user_service_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1501,7 +1833,7 @@ func (x *ListPersonalAccessTokensRequest) String() string {
func (*ListPersonalAccessTokensRequest) ProtoMessage() {}
func (x *ListPersonalAccessTokensRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[19]
mi := &file_api_v1_user_service_proto_msgTypes[25]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1514,7 +1846,7 @@ func (x *ListPersonalAccessTokensRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListPersonalAccessTokensRequest.ProtoReflect.Descriptor instead.
func (*ListPersonalAccessTokensRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{19}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{25}
}
func (x *ListPersonalAccessTokensRequest) GetParent() string {
......@@ -1552,7 +1884,7 @@ type ListPersonalAccessTokensResponse struct {
func (x *ListPersonalAccessTokensResponse) Reset() {
*x = ListPersonalAccessTokensResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[20]
mi := &file_api_v1_user_service_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1564,7 +1896,7 @@ func (x *ListPersonalAccessTokensResponse) String() string {
func (*ListPersonalAccessTokensResponse) ProtoMessage() {}
func (x *ListPersonalAccessTokensResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[20]
mi := &file_api_v1_user_service_proto_msgTypes[26]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1577,7 +1909,7 @@ func (x *ListPersonalAccessTokensResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListPersonalAccessTokensResponse.ProtoReflect.Descriptor instead.
func (*ListPersonalAccessTokensResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{20}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{26}
}
func (x *ListPersonalAccessTokensResponse) GetPersonalAccessTokens() []*PersonalAccessToken {
......@@ -1616,7 +1948,7 @@ type CreatePersonalAccessTokenRequest struct {
func (x *CreatePersonalAccessTokenRequest) Reset() {
*x = CreatePersonalAccessTokenRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[21]
mi := &file_api_v1_user_service_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1628,7 +1960,7 @@ func (x *CreatePersonalAccessTokenRequest) String() string {
func (*CreatePersonalAccessTokenRequest) ProtoMessage() {}
func (x *CreatePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[21]
mi := &file_api_v1_user_service_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1641,7 +1973,7 @@ func (x *CreatePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreatePersonalAccessTokenRequest.ProtoReflect.Descriptor instead.
func (*CreatePersonalAccessTokenRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{21}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{27}
}
func (x *CreatePersonalAccessTokenRequest) GetParent() string {
......@@ -1678,7 +2010,7 @@ type CreatePersonalAccessTokenResponse struct {
func (x *CreatePersonalAccessTokenResponse) Reset() {
*x = CreatePersonalAccessTokenResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[22]
mi := &file_api_v1_user_service_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1690,7 +2022,7 @@ func (x *CreatePersonalAccessTokenResponse) String() string {
func (*CreatePersonalAccessTokenResponse) ProtoMessage() {}
func (x *CreatePersonalAccessTokenResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[22]
mi := &file_api_v1_user_service_proto_msgTypes[28]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1703,7 +2035,7 @@ func (x *CreatePersonalAccessTokenResponse) ProtoReflect() protoreflect.Message
// Deprecated: Use CreatePersonalAccessTokenResponse.ProtoReflect.Descriptor instead.
func (*CreatePersonalAccessTokenResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{22}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{28}
}
func (x *CreatePersonalAccessTokenResponse) GetPersonalAccessToken() *PersonalAccessToken {
......@@ -1731,7 +2063,7 @@ type DeletePersonalAccessTokenRequest struct {
func (x *DeletePersonalAccessTokenRequest) Reset() {
*x = DeletePersonalAccessTokenRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[23]
mi := &file_api_v1_user_service_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1743,7 +2075,7 @@ func (x *DeletePersonalAccessTokenRequest) String() string {
func (*DeletePersonalAccessTokenRequest) ProtoMessage() {}
func (x *DeletePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[23]
mi := &file_api_v1_user_service_proto_msgTypes[29]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1756,7 +2088,7 @@ func (x *DeletePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeletePersonalAccessTokenRequest.ProtoReflect.Descriptor instead.
func (*DeletePersonalAccessTokenRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{23}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{29}
}
func (x *DeletePersonalAccessTokenRequest) GetName() string {
......@@ -1786,7 +2118,7 @@ type UserWebhook struct {
func (x *UserWebhook) Reset() {
*x = UserWebhook{}
mi := &file_api_v1_user_service_proto_msgTypes[24]
mi := &file_api_v1_user_service_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1798,7 +2130,7 @@ func (x *UserWebhook) String() string {
func (*UserWebhook) ProtoMessage() {}
func (x *UserWebhook) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[24]
mi := &file_api_v1_user_service_proto_msgTypes[30]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1811,7 +2143,7 @@ func (x *UserWebhook) ProtoReflect() protoreflect.Message {
// Deprecated: Use UserWebhook.ProtoReflect.Descriptor instead.
func (*UserWebhook) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{24}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{30}
}
func (x *UserWebhook) GetName() string {
......@@ -1860,7 +2192,7 @@ type ListUserWebhooksRequest struct {
func (x *ListUserWebhooksRequest) Reset() {
*x = ListUserWebhooksRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[25]
mi := &file_api_v1_user_service_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1872,7 +2204,7 @@ func (x *ListUserWebhooksRequest) String() string {
func (*ListUserWebhooksRequest) ProtoMessage() {}
func (x *ListUserWebhooksRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[25]
mi := &file_api_v1_user_service_proto_msgTypes[31]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1885,7 +2217,7 @@ func (x *ListUserWebhooksRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListUserWebhooksRequest.ProtoReflect.Descriptor instead.
func (*ListUserWebhooksRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{25}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{31}
}
func (x *ListUserWebhooksRequest) GetParent() string {
......@@ -1905,7 +2237,7 @@ type ListUserWebhooksResponse struct {
func (x *ListUserWebhooksResponse) Reset() {
*x = ListUserWebhooksResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[26]
mi := &file_api_v1_user_service_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1917,7 +2249,7 @@ func (x *ListUserWebhooksResponse) String() string {
func (*ListUserWebhooksResponse) ProtoMessage() {}
func (x *ListUserWebhooksResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[26]
mi := &file_api_v1_user_service_proto_msgTypes[32]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1930,7 +2262,7 @@ func (x *ListUserWebhooksResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListUserWebhooksResponse.ProtoReflect.Descriptor instead.
func (*ListUserWebhooksResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{26}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{32}
}
func (x *ListUserWebhooksResponse) GetWebhooks() []*UserWebhook {
......@@ -1953,7 +2285,7 @@ type CreateUserWebhookRequest struct {
func (x *CreateUserWebhookRequest) Reset() {
*x = CreateUserWebhookRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[27]
mi := &file_api_v1_user_service_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -1965,7 +2297,7 @@ func (x *CreateUserWebhookRequest) String() string {
func (*CreateUserWebhookRequest) ProtoMessage() {}
func (x *CreateUserWebhookRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[27]
mi := &file_api_v1_user_service_proto_msgTypes[33]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -1978,7 +2310,7 @@ func (x *CreateUserWebhookRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateUserWebhookRequest.ProtoReflect.Descriptor instead.
func (*CreateUserWebhookRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{27}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{33}
}
func (x *CreateUserWebhookRequest) GetParent() string {
......@@ -2007,7 +2339,7 @@ type UpdateUserWebhookRequest struct {
func (x *UpdateUserWebhookRequest) Reset() {
*x = UpdateUserWebhookRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[28]
mi := &file_api_v1_user_service_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2019,7 +2351,7 @@ func (x *UpdateUserWebhookRequest) String() string {
func (*UpdateUserWebhookRequest) ProtoMessage() {}
func (x *UpdateUserWebhookRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[28]
mi := &file_api_v1_user_service_proto_msgTypes[34]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2032,7 +2364,7 @@ func (x *UpdateUserWebhookRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateUserWebhookRequest.ProtoReflect.Descriptor instead.
func (*UpdateUserWebhookRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{28}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{34}
}
func (x *UpdateUserWebhookRequest) GetWebhook() *UserWebhook {
......@@ -2060,7 +2392,7 @@ type DeleteUserWebhookRequest struct {
func (x *DeleteUserWebhookRequest) Reset() {
*x = DeleteUserWebhookRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[29]
mi := &file_api_v1_user_service_proto_msgTypes[35]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2072,7 +2404,7 @@ func (x *DeleteUserWebhookRequest) String() string {
func (*DeleteUserWebhookRequest) ProtoMessage() {}
func (x *DeleteUserWebhookRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[29]
mi := &file_api_v1_user_service_proto_msgTypes[35]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2085,7 +2417,7 @@ func (x *DeleteUserWebhookRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeleteUserWebhookRequest.ProtoReflect.Descriptor instead.
func (*DeleteUserWebhookRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{29}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{35}
}
func (x *DeleteUserWebhookRequest) GetName() string {
......@@ -2122,7 +2454,7 @@ type UserNotification struct {
func (x *UserNotification) Reset() {
*x = UserNotification{}
mi := &file_api_v1_user_service_proto_msgTypes[30]
mi := &file_api_v1_user_service_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2134,7 +2466,7 @@ func (x *UserNotification) String() string {
func (*UserNotification) ProtoMessage() {}
func (x *UserNotification) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[30]
mi := &file_api_v1_user_service_proto_msgTypes[36]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2147,7 +2479,7 @@ func (x *UserNotification) ProtoReflect() protoreflect.Message {
// Deprecated: Use UserNotification.ProtoReflect.Descriptor instead.
func (*UserNotification) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{30}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{36}
}
func (x *UserNotification) GetName() string {
......@@ -2247,7 +2579,7 @@ type ListUserNotificationsRequest struct {
func (x *ListUserNotificationsRequest) Reset() {
*x = ListUserNotificationsRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[31]
mi := &file_api_v1_user_service_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2259,7 +2591,7 @@ func (x *ListUserNotificationsRequest) String() string {
func (*ListUserNotificationsRequest) ProtoMessage() {}
func (x *ListUserNotificationsRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[31]
mi := &file_api_v1_user_service_proto_msgTypes[37]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2272,7 +2604,7 @@ func (x *ListUserNotificationsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListUserNotificationsRequest.ProtoReflect.Descriptor instead.
func (*ListUserNotificationsRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{31}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{37}
}
func (x *ListUserNotificationsRequest) GetParent() string {
......@@ -2313,7 +2645,7 @@ type ListUserNotificationsResponse struct {
func (x *ListUserNotificationsResponse) Reset() {
*x = ListUserNotificationsResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[32]
mi := &file_api_v1_user_service_proto_msgTypes[38]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2325,7 +2657,7 @@ func (x *ListUserNotificationsResponse) String() string {
func (*ListUserNotificationsResponse) ProtoMessage() {}
func (x *ListUserNotificationsResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[32]
mi := &file_api_v1_user_service_proto_msgTypes[38]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2338,7 +2670,7 @@ func (x *ListUserNotificationsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListUserNotificationsResponse.ProtoReflect.Descriptor instead.
func (*ListUserNotificationsResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{32}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{38}
}
func (x *ListUserNotificationsResponse) GetNotifications() []*UserNotification {
......@@ -2365,7 +2697,7 @@ type UpdateUserNotificationRequest struct {
func (x *UpdateUserNotificationRequest) Reset() {
*x = UpdateUserNotificationRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[33]
mi := &file_api_v1_user_service_proto_msgTypes[39]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2377,7 +2709,7 @@ func (x *UpdateUserNotificationRequest) String() string {
func (*UpdateUserNotificationRequest) ProtoMessage() {}
func (x *UpdateUserNotificationRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[33]
mi := &file_api_v1_user_service_proto_msgTypes[39]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2390,7 +2722,7 @@ func (x *UpdateUserNotificationRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateUserNotificationRequest.ProtoReflect.Descriptor instead.
func (*UpdateUserNotificationRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{33}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{39}
}
func (x *UpdateUserNotificationRequest) GetNotification() *UserNotification {
......@@ -2417,7 +2749,7 @@ type DeleteUserNotificationRequest struct {
func (x *DeleteUserNotificationRequest) Reset() {
*x = DeleteUserNotificationRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[34]
mi := &file_api_v1_user_service_proto_msgTypes[40]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2429,7 +2761,7 @@ func (x *DeleteUserNotificationRequest) String() string {
func (*DeleteUserNotificationRequest) ProtoMessage() {}
func (x *DeleteUserNotificationRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[34]
mi := &file_api_v1_user_service_proto_msgTypes[40]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2442,7 +2774,7 @@ func (x *DeleteUserNotificationRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeleteUserNotificationRequest.ProtoReflect.Descriptor instead.
func (*DeleteUserNotificationRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{34}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{40}
}
func (x *DeleteUserNotificationRequest) GetName() string {
......@@ -2465,7 +2797,7 @@ type UserStats_MemoTypeStats struct {
func (x *UserStats_MemoTypeStats) Reset() {
*x = UserStats_MemoTypeStats{}
mi := &file_api_v1_user_service_proto_msgTypes[36]
mi := &file_api_v1_user_service_proto_msgTypes[41]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2477,7 +2809,7 @@ func (x *UserStats_MemoTypeStats) String() string {
func (*UserStats_MemoTypeStats) ProtoMessage() {}
func (x *UserStats_MemoTypeStats) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[36]
mi := &file_api_v1_user_service_proto_msgTypes[41]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2490,7 +2822,7 @@ func (x *UserStats_MemoTypeStats) ProtoReflect() protoreflect.Message {
// Deprecated: Use UserStats_MemoTypeStats.ProtoReflect.Descriptor instead.
func (*UserStats_MemoTypeStats) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{9, 1}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{9, 0}
}
func (x *UserStats_MemoTypeStats) GetLinkCount() int32 {
......@@ -2538,7 +2870,7 @@ type UserSetting_GeneralSetting struct {
func (x *UserSetting_GeneralSetting) Reset() {
*x = UserSetting_GeneralSetting{}
mi := &file_api_v1_user_service_proto_msgTypes[37]
mi := &file_api_v1_user_service_proto_msgTypes[43]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2550,7 +2882,7 @@ func (x *UserSetting_GeneralSetting) String() string {
func (*UserSetting_GeneralSetting) ProtoMessage() {}
func (x *UserSetting_GeneralSetting) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[37]
mi := &file_api_v1_user_service_proto_msgTypes[43]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2598,7 +2930,7 @@ type UserSetting_WebhooksSetting struct {
func (x *UserSetting_WebhooksSetting) Reset() {
*x = UserSetting_WebhooksSetting{}
mi := &file_api_v1_user_service_proto_msgTypes[38]
mi := &file_api_v1_user_service_proto_msgTypes[44]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2610,7 +2942,7 @@ func (x *UserSetting_WebhooksSetting) String() string {
func (*UserSetting_WebhooksSetting) ProtoMessage() {}
func (x *UserSetting_WebhooksSetting) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[38]
mi := &file_api_v1_user_service_proto_msgTypes[44]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2651,7 +2983,7 @@ type UserNotification_MemoCommentPayload struct {
func (x *UserNotification_MemoCommentPayload) Reset() {
*x = UserNotification_MemoCommentPayload{}
mi := &file_api_v1_user_service_proto_msgTypes[39]
mi := &file_api_v1_user_service_proto_msgTypes[45]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2663,7 +2995,7 @@ func (x *UserNotification_MemoCommentPayload) String() string {
func (*UserNotification_MemoCommentPayload) ProtoMessage() {}
func (x *UserNotification_MemoCommentPayload) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[39]
mi := &file_api_v1_user_service_proto_msgTypes[45]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2676,7 +3008,7 @@ func (x *UserNotification_MemoCommentPayload) ProtoReflect() protoreflect.Messag
// Deprecated: Use UserNotification_MemoCommentPayload.ProtoReflect.Descriptor instead.
func (*UserNotification_MemoCommentPayload) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{30, 0}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{36, 0}
}
func (x *UserNotification_MemoCommentPayload) GetMemo() string {
......@@ -2725,7 +3057,7 @@ type UserNotification_MemoMentionPayload struct {
func (x *UserNotification_MemoMentionPayload) Reset() {
*x = UserNotification_MemoMentionPayload{}
mi := &file_api_v1_user_service_proto_msgTypes[40]
mi := &file_api_v1_user_service_proto_msgTypes[46]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
......@@ -2737,7 +3069,7 @@ func (x *UserNotification_MemoMentionPayload) String() string {
func (*UserNotification_MemoMentionPayload) ProtoMessage() {}
func (x *UserNotification_MemoMentionPayload) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[40]
mi := &file_api_v1_user_service_proto_msgTypes[46]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
......@@ -2750,7 +3082,7 @@ func (x *UserNotification_MemoMentionPayload) ProtoReflect() protoreflect.Messag
// Deprecated: Use UserNotification_MemoMentionPayload.ProtoReflect.Descriptor instead.
func (*UserNotification_MemoMentionPayload) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{30, 1}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{36, 1}
}
func (x *UserNotification_MemoMentionPayload) GetMemo() string {
......@@ -2847,10 +3179,7 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"\x0fmemo_type_stats\x18\x03 \x01(\v2%.memos.api.v1.UserStats.MemoTypeStatsR\rmemoTypeStats\x12B\n" +
"\ttag_count\x18\x04 \x03(\v2%.memos.api.v1.UserStats.TagCountEntryR\btagCount\x12!\n" +
"\fpinned_memos\x18\x05 \x03(\tR\vpinnedMemos\x12(\n" +
"\x10total_memo_count\x18\x06 \x01(\x05R\x0etotalMemoCount\x1a;\n" +
"\rTagCountEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\x05R\x05value:\x028\x01\x1a\x8b\x01\n" +
"\x10total_memo_count\x18\x06 \x01(\x05R\x0etotalMemoCount\x1a\x8b\x01\n" +
"\rMemoTypeStats\x12\x1d\n" +
"\n" +
"link_count\x18\x01 \x01(\x05R\tlinkCount\x12\x1d\n" +
......@@ -2859,7 +3188,10 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"\n" +
"todo_count\x18\x03 \x01(\x05R\ttodoCount\x12\x1d\n" +
"\n" +
"undo_count\x18\x04 \x01(\x05R\tundoCount:?\xeaA<\n" +
"undo_count\x18\x04 \x01(\x05R\tundoCount\x1a;\n" +
"\rTagCountEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\x05R\x05value:\x028\x01:?\xeaA<\n" +
"\x16memos.api.v1/UserStats\x12\fusers/{user}*\tuserStats2\tuserStats\"D\n" +
"\x13GetUserStatsRequest\x12-\n" +
"\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" +
......@@ -2900,7 +3232,33 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"\bsettings\x18\x01 \x03(\v2\x19.memos.api.v1.UserSettingR\bsettings\x12&\n" +
"\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" +
"\n" +
"total_size\x18\x03 \x01(\x05R\ttotalSize\"\xa7\x03\n" +
"total_size\x18\x03 \x01(\x05R\ttotalSize\"\x84\x02\n" +
"\x0eLinkedIdentity\x12\x17\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12@\n" +
"\bidp_name\x18\x02 \x01(\tB%\xe0A\x03\xfaA\x1f\n" +
"\x1dmemos.api.v1/IdentityProviderR\aidpName\x12\"\n" +
"\n" +
"extern_uid\x18\x03 \x01(\tB\x03\xe0A\x03R\texternUid:s\xeaAp\n" +
"\x1bmemos.api.v1/LinkedIdentity\x12/users/{user}/linkedIdentities/{linked_identity}*\x10linkedIdentities2\x0elinkedIdentity\"P\n" +
"\x1bListLinkedIdentitiesRequest\x121\n" +
"\x06parent\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" +
"\x11memos.api.v1/UserR\x06parent\"i\n" +
"\x1cListLinkedIdentitiesResponse\x12I\n" +
"\x11linked_identities\x18\x01 \x03(\v2\x1c.memos.api.v1.LinkedIdentityR\x10linkedIdentities\"\xfd\x01\n" +
"\x1bCreateLinkedIdentityRequest\x121\n" +
"\x06parent\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" +
"\x11memos.api.v1/UserR\x06parent\x12@\n" +
"\bidp_name\x18\x02 \x01(\tB%\xe0A\x02\xfaA\x1f\n" +
"\x1dmemos.api.v1/IdentityProviderR\aidpName\x12\x17\n" +
"\x04code\x18\x03 \x01(\tB\x03\xe0A\x02R\x04code\x12&\n" +
"\fredirect_uri\x18\x04 \x01(\tB\x03\xe0A\x02R\vredirectUri\x12(\n" +
"\rcode_verifier\x18\x05 \x01(\tB\x03\xe0A\x01R\fcodeVerifier\"S\n" +
"\x18GetLinkedIdentityRequest\x127\n" +
"\x04name\x18\x01 \x01(\tB#\xe0A\x02\xfaA\x1d\n" +
"\x1bmemos.api.v1/LinkedIdentityR\x04name\"V\n" +
"\x1bDeleteLinkedIdentityRequest\x127\n" +
"\x04name\x18\x01 \x01(\tB#\xe0A\x02\xfaA\x1d\n" +
"\x1bmemos.api.v1/LinkedIdentityR\x04name\"\xa7\x03\n" +
"\x13PersonalAccessToken\x12\x17\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12%\n" +
"\vdescription\x18\x02 \x01(\tB\x03\xe0A\x01R\vdescription\x12>\n" +
......@@ -3003,7 +3361,7 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"updateMask\"Z\n" +
"\x1dDeleteUserNotificationRequest\x129\n" +
"\x04name\x18\x01 \x01(\tB%\xe0A\x02\xfaA\x1f\n" +
"\x1dmemos.api.v1/UserNotificationR\x04name2\x80\x18\n" +
"\x1dmemos.api.v1/UserNotificationR\x04name2\x82\x1d\n" +
"\vUserService\x12c\n" +
"\tListUsers\x12\x1e.memos.api.v1.ListUsersRequest\x1a\x1f.memos.api.v1.ListUsersResponse\"\x15\x82\xd3\xe4\x93\x02\x0f\x12\r/api/v1/users\x12{\n" +
"\rBatchGetUsers\x12\".memos.api.v1.BatchGetUsersRequest\x1a#.memos.api.v1.BatchGetUsersResponse\"!\x82\xd3\xe4\x93\x02\x1b:\x01*\"\x16/api/v1/users:batchGet\x12b\n" +
......@@ -3018,7 +3376,11 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"\fGetUserStats\x12!.memos.api.v1.GetUserStatsRequest\x1a\x17.memos.api.v1.UserStats\".\xdaA\x04name\x82\xd3\xe4\x93\x02!\x12\x1f/api/v1/{name=users/*}:getStats\x12\x82\x01\n" +
"\x0eGetUserSetting\x12#.memos.api.v1.GetUserSettingRequest\x1a\x19.memos.api.v1.UserSetting\"0\xdaA\x04name\x82\xd3\xe4\x93\x02#\x12!/api/v1/{name=users/*/settings/*}\x12\xa8\x01\n" +
"\x11UpdateUserSetting\x12&.memos.api.v1.UpdateUserSettingRequest\x1a\x19.memos.api.v1.UserSetting\"P\xdaA\x13setting,update_mask\x82\xd3\xe4\x93\x024:\asetting2)/api/v1/{setting.name=users/*/settings/*}\x12\x95\x01\n" +
"\x10ListUserSettings\x12%.memos.api.v1.ListUserSettingsRequest\x1a&.memos.api.v1.ListUserSettingsResponse\"2\xdaA\x06parent\x82\xd3\xe4\x93\x02#\x12!/api/v1/{parent=users/*}/settings\x12\xb9\x01\n" +
"\x10ListUserSettings\x12%.memos.api.v1.ListUserSettingsRequest\x1a&.memos.api.v1.ListUserSettingsResponse\"2\xdaA\x06parent\x82\xd3\xe4\x93\x02#\x12!/api/v1/{parent=users/*}/settings\x12\xa9\x01\n" +
"\x14ListLinkedIdentities\x12).memos.api.v1.ListLinkedIdentitiesRequest\x1a*.memos.api.v1.ListLinkedIdentitiesResponse\":\xdaA\x06parent\x82\xd3\xe4\x93\x02+\x12)/api/v1/{parent=users/*}/linkedIdentities\x12\xa7\x01\n" +
"\x14CreateLinkedIdentity\x12).memos.api.v1.CreateLinkedIdentityRequest\x1a\x1c.memos.api.v1.LinkedIdentity\"F\xdaA\x0fparent,idp_name\x82\xd3\xe4\x93\x02.:\x01*\")/api/v1/{parent=users/*}/linkedIdentities\x12\x93\x01\n" +
"\x11GetLinkedIdentity\x12&.memos.api.v1.GetLinkedIdentityRequest\x1a\x1c.memos.api.v1.LinkedIdentity\"8\xdaA\x04name\x82\xd3\xe4\x93\x02+\x12)/api/v1/{name=users/*/linkedIdentities/*}\x12\x93\x01\n" +
"\x14DeleteLinkedIdentity\x12).memos.api.v1.DeleteLinkedIdentityRequest\x1a\x16.google.protobuf.Empty\"8\xdaA\x04name\x82\xd3\xe4\x93\x02+*)/api/v1/{name=users/*/linkedIdentities/*}\x12\xb9\x01\n" +
"\x18ListPersonalAccessTokens\x12-.memos.api.v1.ListPersonalAccessTokensRequest\x1a..memos.api.v1.ListPersonalAccessTokensResponse\">\xdaA\x06parent\x82\xd3\xe4\x93\x02/\x12-/api/v1/{parent=users/*}/personalAccessTokens\x12\xb6\x01\n" +
"\x19CreatePersonalAccessToken\x12..memos.api.v1.CreatePersonalAccessTokenRequest\x1a/.memos.api.v1.CreatePersonalAccessTokenResponse\"8\x82\xd3\xe4\x93\x022:\x01*\"-/api/v1/{parent=users/*}/personalAccessTokens\x12\xa1\x01\n" +
"\x19DeletePersonalAccessToken\x12..memos.api.v1.DeletePersonalAccessTokenRequest\x1a\x16.google.protobuf.Empty\"<\xdaA\x04name\x82\xd3\xe4\x93\x02/*-/api/v1/{name=users/*/personalAccessTokens/*}\x12\x95\x01\n" +
......@@ -3044,7 +3406,7 @@ func file_api_v1_user_service_proto_rawDescGZIP() []byte {
}
var file_api_v1_user_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 41)
var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 47)
var file_api_v1_user_service_proto_goTypes = []any{
(User_Role)(0), // 0: memos.api.v1.User.Role
(UserSetting_Key)(0), // 1: memos.api.v1.UserSetting.Key
......@@ -3068,122 +3430,137 @@ var file_api_v1_user_service_proto_goTypes = []any{
(*UpdateUserSettingRequest)(nil), // 19: memos.api.v1.UpdateUserSettingRequest
(*ListUserSettingsRequest)(nil), // 20: memos.api.v1.ListUserSettingsRequest
(*ListUserSettingsResponse)(nil), // 21: memos.api.v1.ListUserSettingsResponse
(*PersonalAccessToken)(nil), // 22: memos.api.v1.PersonalAccessToken
(*ListPersonalAccessTokensRequest)(nil), // 23: memos.api.v1.ListPersonalAccessTokensRequest
(*ListPersonalAccessTokensResponse)(nil), // 24: memos.api.v1.ListPersonalAccessTokensResponse
(*CreatePersonalAccessTokenRequest)(nil), // 25: memos.api.v1.CreatePersonalAccessTokenRequest
(*CreatePersonalAccessTokenResponse)(nil), // 26: memos.api.v1.CreatePersonalAccessTokenResponse
(*DeletePersonalAccessTokenRequest)(nil), // 27: memos.api.v1.DeletePersonalAccessTokenRequest
(*UserWebhook)(nil), // 28: memos.api.v1.UserWebhook
(*ListUserWebhooksRequest)(nil), // 29: memos.api.v1.ListUserWebhooksRequest
(*ListUserWebhooksResponse)(nil), // 30: memos.api.v1.ListUserWebhooksResponse
(*CreateUserWebhookRequest)(nil), // 31: memos.api.v1.CreateUserWebhookRequest
(*UpdateUserWebhookRequest)(nil), // 32: memos.api.v1.UpdateUserWebhookRequest
(*DeleteUserWebhookRequest)(nil), // 33: memos.api.v1.DeleteUserWebhookRequest
(*UserNotification)(nil), // 34: memos.api.v1.UserNotification
(*ListUserNotificationsRequest)(nil), // 35: memos.api.v1.ListUserNotificationsRequest
(*ListUserNotificationsResponse)(nil), // 36: memos.api.v1.ListUserNotificationsResponse
(*UpdateUserNotificationRequest)(nil), // 37: memos.api.v1.UpdateUserNotificationRequest
(*DeleteUserNotificationRequest)(nil), // 38: memos.api.v1.DeleteUserNotificationRequest
nil, // 39: memos.api.v1.UserStats.TagCountEntry
(*UserStats_MemoTypeStats)(nil), // 40: memos.api.v1.UserStats.MemoTypeStats
(*UserSetting_GeneralSetting)(nil), // 41: memos.api.v1.UserSetting.GeneralSetting
(*UserSetting_WebhooksSetting)(nil), // 42: memos.api.v1.UserSetting.WebhooksSetting
(*UserNotification_MemoCommentPayload)(nil), // 43: memos.api.v1.UserNotification.MemoCommentPayload
(*UserNotification_MemoMentionPayload)(nil), // 44: memos.api.v1.UserNotification.MemoMentionPayload
(State)(0), // 45: memos.api.v1.State
(*timestamppb.Timestamp)(nil), // 46: google.protobuf.Timestamp
(*fieldmaskpb.FieldMask)(nil), // 47: google.protobuf.FieldMask
(*emptypb.Empty)(nil), // 48: google.protobuf.Empty
(*LinkedIdentity)(nil), // 22: memos.api.v1.LinkedIdentity
(*ListLinkedIdentitiesRequest)(nil), // 23: memos.api.v1.ListLinkedIdentitiesRequest
(*ListLinkedIdentitiesResponse)(nil), // 24: memos.api.v1.ListLinkedIdentitiesResponse
(*CreateLinkedIdentityRequest)(nil), // 25: memos.api.v1.CreateLinkedIdentityRequest
(*GetLinkedIdentityRequest)(nil), // 26: memos.api.v1.GetLinkedIdentityRequest
(*DeleteLinkedIdentityRequest)(nil), // 27: memos.api.v1.DeleteLinkedIdentityRequest
(*PersonalAccessToken)(nil), // 28: memos.api.v1.PersonalAccessToken
(*ListPersonalAccessTokensRequest)(nil), // 29: memos.api.v1.ListPersonalAccessTokensRequest
(*ListPersonalAccessTokensResponse)(nil), // 30: memos.api.v1.ListPersonalAccessTokensResponse
(*CreatePersonalAccessTokenRequest)(nil), // 31: memos.api.v1.CreatePersonalAccessTokenRequest
(*CreatePersonalAccessTokenResponse)(nil), // 32: memos.api.v1.CreatePersonalAccessTokenResponse
(*DeletePersonalAccessTokenRequest)(nil), // 33: memos.api.v1.DeletePersonalAccessTokenRequest
(*UserWebhook)(nil), // 34: memos.api.v1.UserWebhook
(*ListUserWebhooksRequest)(nil), // 35: memos.api.v1.ListUserWebhooksRequest
(*ListUserWebhooksResponse)(nil), // 36: memos.api.v1.ListUserWebhooksResponse
(*CreateUserWebhookRequest)(nil), // 37: memos.api.v1.CreateUserWebhookRequest
(*UpdateUserWebhookRequest)(nil), // 38: memos.api.v1.UpdateUserWebhookRequest
(*DeleteUserWebhookRequest)(nil), // 39: memos.api.v1.DeleteUserWebhookRequest
(*UserNotification)(nil), // 40: memos.api.v1.UserNotification
(*ListUserNotificationsRequest)(nil), // 41: memos.api.v1.ListUserNotificationsRequest
(*ListUserNotificationsResponse)(nil), // 42: memos.api.v1.ListUserNotificationsResponse
(*UpdateUserNotificationRequest)(nil), // 43: memos.api.v1.UpdateUserNotificationRequest
(*DeleteUserNotificationRequest)(nil), // 44: memos.api.v1.DeleteUserNotificationRequest
(*UserStats_MemoTypeStats)(nil), // 45: memos.api.v1.UserStats.MemoTypeStats
nil, // 46: memos.api.v1.UserStats.TagCountEntry
(*UserSetting_GeneralSetting)(nil), // 47: memos.api.v1.UserSetting.GeneralSetting
(*UserSetting_WebhooksSetting)(nil), // 48: memos.api.v1.UserSetting.WebhooksSetting
(*UserNotification_MemoCommentPayload)(nil), // 49: memos.api.v1.UserNotification.MemoCommentPayload
(*UserNotification_MemoMentionPayload)(nil), // 50: memos.api.v1.UserNotification.MemoMentionPayload
(State)(0), // 51: memos.api.v1.State
(*timestamppb.Timestamp)(nil), // 52: google.protobuf.Timestamp
(*fieldmaskpb.FieldMask)(nil), // 53: google.protobuf.FieldMask
(*emptypb.Empty)(nil), // 54: google.protobuf.Empty
}
var file_api_v1_user_service_proto_depIdxs = []int32{
0, // 0: memos.api.v1.User.role:type_name -> memos.api.v1.User.Role
45, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State
46, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp
46, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp
51, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State
52, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp
52, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp
4, // 4: memos.api.v1.ListUsersResponse.users:type_name -> memos.api.v1.User
4, // 5: memos.api.v1.BatchGetUsersResponse.users:type_name -> memos.api.v1.User
47, // 6: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask
53, // 6: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask
4, // 7: memos.api.v1.CreateUserRequest.user:type_name -> memos.api.v1.User
4, // 8: memos.api.v1.UpdateUserRequest.user:type_name -> memos.api.v1.User
47, // 9: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask
46, // 10: memos.api.v1.UserStats.memo_display_timestamps:type_name -> google.protobuf.Timestamp
40, // 11: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats
39, // 12: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry
53, // 9: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask
52, // 10: memos.api.v1.UserStats.memo_display_timestamps:type_name -> google.protobuf.Timestamp
45, // 11: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats
46, // 12: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry
13, // 13: memos.api.v1.ListAllUserStatsResponse.stats:type_name -> memos.api.v1.UserStats
41, // 14: memos.api.v1.UserSetting.general_setting:type_name -> memos.api.v1.UserSetting.GeneralSetting
42, // 15: memos.api.v1.UserSetting.webhooks_setting:type_name -> memos.api.v1.UserSetting.WebhooksSetting
47, // 14: memos.api.v1.UserSetting.general_setting:type_name -> memos.api.v1.UserSetting.GeneralSetting
48, // 15: memos.api.v1.UserSetting.webhooks_setting:type_name -> memos.api.v1.UserSetting.WebhooksSetting
17, // 16: memos.api.v1.UpdateUserSettingRequest.setting:type_name -> memos.api.v1.UserSetting
47, // 17: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask
53, // 17: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask
17, // 18: memos.api.v1.ListUserSettingsResponse.settings:type_name -> memos.api.v1.UserSetting
46, // 19: memos.api.v1.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
46, // 20: memos.api.v1.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
46, // 21: memos.api.v1.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
22, // 22: memos.api.v1.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.v1.PersonalAccessToken
22, // 23: memos.api.v1.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.v1.PersonalAccessToken
46, // 24: memos.api.v1.UserWebhook.create_time:type_name -> google.protobuf.Timestamp
46, // 25: memos.api.v1.UserWebhook.update_time:type_name -> google.protobuf.Timestamp
28, // 26: memos.api.v1.ListUserWebhooksResponse.webhooks:type_name -> memos.api.v1.UserWebhook
28, // 27: memos.api.v1.CreateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
28, // 28: memos.api.v1.UpdateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
47, // 29: memos.api.v1.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask
4, // 30: memos.api.v1.UserNotification.sender_user:type_name -> memos.api.v1.User
2, // 31: memos.api.v1.UserNotification.status:type_name -> memos.api.v1.UserNotification.Status
46, // 32: memos.api.v1.UserNotification.create_time:type_name -> google.protobuf.Timestamp
3, // 33: memos.api.v1.UserNotification.type:type_name -> memos.api.v1.UserNotification.Type
43, // 34: memos.api.v1.UserNotification.memo_comment:type_name -> memos.api.v1.UserNotification.MemoCommentPayload
44, // 35: memos.api.v1.UserNotification.memo_mention:type_name -> memos.api.v1.UserNotification.MemoMentionPayload
34, // 36: memos.api.v1.ListUserNotificationsResponse.notifications:type_name -> memos.api.v1.UserNotification
34, // 37: memos.api.v1.UpdateUserNotificationRequest.notification:type_name -> memos.api.v1.UserNotification
47, // 38: memos.api.v1.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask
28, // 39: memos.api.v1.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.v1.UserWebhook
5, // 40: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest
7, // 41: memos.api.v1.UserService.BatchGetUsers:input_type -> memos.api.v1.BatchGetUsersRequest
9, // 42: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest
10, // 43: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest
11, // 44: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest
12, // 45: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest
15, // 46: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest
14, // 47: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest
18, // 48: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest
19, // 49: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest
20, // 50: memos.api.v1.UserService.ListUserSettings:input_type -> memos.api.v1.ListUserSettingsRequest
23, // 51: memos.api.v1.UserService.ListPersonalAccessTokens:input_type -> memos.api.v1.ListPersonalAccessTokensRequest
25, // 52: memos.api.v1.UserService.CreatePersonalAccessToken:input_type -> memos.api.v1.CreatePersonalAccessTokenRequest
27, // 53: memos.api.v1.UserService.DeletePersonalAccessToken:input_type -> memos.api.v1.DeletePersonalAccessTokenRequest
29, // 54: memos.api.v1.UserService.ListUserWebhooks:input_type -> memos.api.v1.ListUserWebhooksRequest
31, // 55: memos.api.v1.UserService.CreateUserWebhook:input_type -> memos.api.v1.CreateUserWebhookRequest
32, // 56: memos.api.v1.UserService.UpdateUserWebhook:input_type -> memos.api.v1.UpdateUserWebhookRequest
33, // 57: memos.api.v1.UserService.DeleteUserWebhook:input_type -> memos.api.v1.DeleteUserWebhookRequest
35, // 58: memos.api.v1.UserService.ListUserNotifications:input_type -> memos.api.v1.ListUserNotificationsRequest
37, // 59: memos.api.v1.UserService.UpdateUserNotification:input_type -> memos.api.v1.UpdateUserNotificationRequest
38, // 60: memos.api.v1.UserService.DeleteUserNotification:input_type -> memos.api.v1.DeleteUserNotificationRequest
6, // 61: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse
8, // 62: memos.api.v1.UserService.BatchGetUsers:output_type -> memos.api.v1.BatchGetUsersResponse
4, // 63: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User
4, // 64: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User
4, // 65: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User
48, // 66: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty
16, // 67: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse
13, // 68: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats
17, // 69: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting
17, // 70: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting
21, // 71: memos.api.v1.UserService.ListUserSettings:output_type -> memos.api.v1.ListUserSettingsResponse
24, // 72: memos.api.v1.UserService.ListPersonalAccessTokens:output_type -> memos.api.v1.ListPersonalAccessTokensResponse
26, // 73: memos.api.v1.UserService.CreatePersonalAccessToken:output_type -> memos.api.v1.CreatePersonalAccessTokenResponse
48, // 74: memos.api.v1.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty
30, // 75: memos.api.v1.UserService.ListUserWebhooks:output_type -> memos.api.v1.ListUserWebhooksResponse
28, // 76: memos.api.v1.UserService.CreateUserWebhook:output_type -> memos.api.v1.UserWebhook
28, // 77: memos.api.v1.UserService.UpdateUserWebhook:output_type -> memos.api.v1.UserWebhook
48, // 78: memos.api.v1.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty
36, // 79: memos.api.v1.UserService.ListUserNotifications:output_type -> memos.api.v1.ListUserNotificationsResponse
34, // 80: memos.api.v1.UserService.UpdateUserNotification:output_type -> memos.api.v1.UserNotification
48, // 81: memos.api.v1.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty
61, // [61:82] is the sub-list for method output_type
40, // [40:61] is the sub-list for method input_type
40, // [40:40] is the sub-list for extension type_name
40, // [40:40] is the sub-list for extension extendee
0, // [0:40] is the sub-list for field type_name
22, // 19: memos.api.v1.ListLinkedIdentitiesResponse.linked_identities:type_name -> memos.api.v1.LinkedIdentity
52, // 20: memos.api.v1.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
52, // 21: memos.api.v1.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
52, // 22: memos.api.v1.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
28, // 23: memos.api.v1.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.v1.PersonalAccessToken
28, // 24: memos.api.v1.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.v1.PersonalAccessToken
52, // 25: memos.api.v1.UserWebhook.create_time:type_name -> google.protobuf.Timestamp
52, // 26: memos.api.v1.UserWebhook.update_time:type_name -> google.protobuf.Timestamp
34, // 27: memos.api.v1.ListUserWebhooksResponse.webhooks:type_name -> memos.api.v1.UserWebhook
34, // 28: memos.api.v1.CreateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
34, // 29: memos.api.v1.UpdateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
53, // 30: memos.api.v1.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask
4, // 31: memos.api.v1.UserNotification.sender_user:type_name -> memos.api.v1.User
2, // 32: memos.api.v1.UserNotification.status:type_name -> memos.api.v1.UserNotification.Status
52, // 33: memos.api.v1.UserNotification.create_time:type_name -> google.protobuf.Timestamp
3, // 34: memos.api.v1.UserNotification.type:type_name -> memos.api.v1.UserNotification.Type
49, // 35: memos.api.v1.UserNotification.memo_comment:type_name -> memos.api.v1.UserNotification.MemoCommentPayload
50, // 36: memos.api.v1.UserNotification.memo_mention:type_name -> memos.api.v1.UserNotification.MemoMentionPayload
40, // 37: memos.api.v1.ListUserNotificationsResponse.notifications:type_name -> memos.api.v1.UserNotification
40, // 38: memos.api.v1.UpdateUserNotificationRequest.notification:type_name -> memos.api.v1.UserNotification
53, // 39: memos.api.v1.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask
34, // 40: memos.api.v1.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.v1.UserWebhook
5, // 41: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest
7, // 42: memos.api.v1.UserService.BatchGetUsers:input_type -> memos.api.v1.BatchGetUsersRequest
9, // 43: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest
10, // 44: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest
11, // 45: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest
12, // 46: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest
15, // 47: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest
14, // 48: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest
18, // 49: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest
19, // 50: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest
20, // 51: memos.api.v1.UserService.ListUserSettings:input_type -> memos.api.v1.ListUserSettingsRequest
23, // 52: memos.api.v1.UserService.ListLinkedIdentities:input_type -> memos.api.v1.ListLinkedIdentitiesRequest
25, // 53: memos.api.v1.UserService.CreateLinkedIdentity:input_type -> memos.api.v1.CreateLinkedIdentityRequest
26, // 54: memos.api.v1.UserService.GetLinkedIdentity:input_type -> memos.api.v1.GetLinkedIdentityRequest
27, // 55: memos.api.v1.UserService.DeleteLinkedIdentity:input_type -> memos.api.v1.DeleteLinkedIdentityRequest
29, // 56: memos.api.v1.UserService.ListPersonalAccessTokens:input_type -> memos.api.v1.ListPersonalAccessTokensRequest
31, // 57: memos.api.v1.UserService.CreatePersonalAccessToken:input_type -> memos.api.v1.CreatePersonalAccessTokenRequest
33, // 58: memos.api.v1.UserService.DeletePersonalAccessToken:input_type -> memos.api.v1.DeletePersonalAccessTokenRequest
35, // 59: memos.api.v1.UserService.ListUserWebhooks:input_type -> memos.api.v1.ListUserWebhooksRequest
37, // 60: memos.api.v1.UserService.CreateUserWebhook:input_type -> memos.api.v1.CreateUserWebhookRequest
38, // 61: memos.api.v1.UserService.UpdateUserWebhook:input_type -> memos.api.v1.UpdateUserWebhookRequest
39, // 62: memos.api.v1.UserService.DeleteUserWebhook:input_type -> memos.api.v1.DeleteUserWebhookRequest
41, // 63: memos.api.v1.UserService.ListUserNotifications:input_type -> memos.api.v1.ListUserNotificationsRequest
43, // 64: memos.api.v1.UserService.UpdateUserNotification:input_type -> memos.api.v1.UpdateUserNotificationRequest
44, // 65: memos.api.v1.UserService.DeleteUserNotification:input_type -> memos.api.v1.DeleteUserNotificationRequest
6, // 66: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse
8, // 67: memos.api.v1.UserService.BatchGetUsers:output_type -> memos.api.v1.BatchGetUsersResponse
4, // 68: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User
4, // 69: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User
4, // 70: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User
54, // 71: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty
16, // 72: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse
13, // 73: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats
17, // 74: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting
17, // 75: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting
21, // 76: memos.api.v1.UserService.ListUserSettings:output_type -> memos.api.v1.ListUserSettingsResponse
24, // 77: memos.api.v1.UserService.ListLinkedIdentities:output_type -> memos.api.v1.ListLinkedIdentitiesResponse
22, // 78: memos.api.v1.UserService.CreateLinkedIdentity:output_type -> memos.api.v1.LinkedIdentity
22, // 79: memos.api.v1.UserService.GetLinkedIdentity:output_type -> memos.api.v1.LinkedIdentity
54, // 80: memos.api.v1.UserService.DeleteLinkedIdentity:output_type -> google.protobuf.Empty
30, // 81: memos.api.v1.UserService.ListPersonalAccessTokens:output_type -> memos.api.v1.ListPersonalAccessTokensResponse
32, // 82: memos.api.v1.UserService.CreatePersonalAccessToken:output_type -> memos.api.v1.CreatePersonalAccessTokenResponse
54, // 83: memos.api.v1.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty
36, // 84: memos.api.v1.UserService.ListUserWebhooks:output_type -> memos.api.v1.ListUserWebhooksResponse
34, // 85: memos.api.v1.UserService.CreateUserWebhook:output_type -> memos.api.v1.UserWebhook
34, // 86: memos.api.v1.UserService.UpdateUserWebhook:output_type -> memos.api.v1.UserWebhook
54, // 87: memos.api.v1.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty
42, // 88: memos.api.v1.UserService.ListUserNotifications:output_type -> memos.api.v1.ListUserNotificationsResponse
40, // 89: memos.api.v1.UserService.UpdateUserNotification:output_type -> memos.api.v1.UserNotification
54, // 90: memos.api.v1.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty
66, // [66:91] is the sub-list for method output_type
41, // [41:66] is the sub-list for method input_type
41, // [41:41] is the sub-list for extension type_name
41, // [41:41] is the sub-list for extension extendee
0, // [0:41] is the sub-list for field type_name
}
func init() { file_api_v1_user_service_proto_init() }
......@@ -3196,7 +3573,7 @@ func file_api_v1_user_service_proto_init() {
(*UserSetting_GeneralSetting_)(nil),
(*UserSetting_WebhooksSetting_)(nil),
}
file_api_v1_user_service_proto_msgTypes[30].OneofWrappers = []any{
file_api_v1_user_service_proto_msgTypes[36].OneofWrappers = []any{
(*UserNotification_MemoComment)(nil),
(*UserNotification_MemoMention)(nil),
}
......@@ -3206,7 +3583,7 @@ func file_api_v1_user_service_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_user_service_proto_rawDesc), len(file_api_v1_user_service_proto_rawDesc)),
NumEnums: 4,
NumMessages: 41,
NumMessages: 47,
NumExtensions: 0,
NumServices: 1,
},
......
......@@ -558,6 +558,168 @@ func local_request_UserService_ListUserSettings_0(ctx context.Context, marshaler
return msg, metadata, err
}
func request_UserService_ListLinkedIdentities_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ListLinkedIdentitiesRequest
metadata runtime.ServerMetadata
err error
)
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
val, ok := pathParams["parent"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent")
}
protoReq.Parent, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
}
msg, err := client.ListLinkedIdentities(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_UserService_ListLinkedIdentities_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ListLinkedIdentitiesRequest
metadata runtime.ServerMetadata
err error
)
val, ok := pathParams["parent"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent")
}
protoReq.Parent, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
}
msg, err := server.ListLinkedIdentities(ctx, &protoReq)
return msg, metadata, err
}
func request_UserService_CreateLinkedIdentity_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq CreateLinkedIdentityRequest
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
val, ok := pathParams["parent"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent")
}
protoReq.Parent, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
}
msg, err := client.CreateLinkedIdentity(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_UserService_CreateLinkedIdentity_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq CreateLinkedIdentityRequest
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
val, ok := pathParams["parent"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent")
}
protoReq.Parent, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
}
msg, err := server.CreateLinkedIdentity(ctx, &protoReq)
return msg, metadata, err
}
func request_UserService_GetLinkedIdentity_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetLinkedIdentityRequest
metadata runtime.ServerMetadata
err error
)
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
val, ok := pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := client.GetLinkedIdentity(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_UserService_GetLinkedIdentity_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetLinkedIdentityRequest
metadata runtime.ServerMetadata
err error
)
val, ok := pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := server.GetLinkedIdentity(ctx, &protoReq)
return msg, metadata, err
}
func request_UserService_DeleteLinkedIdentity_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq DeleteLinkedIdentityRequest
metadata runtime.ServerMetadata
err error
)
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
val, ok := pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := client.DeleteLinkedIdentity(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_UserService_DeleteLinkedIdentity_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq DeleteLinkedIdentityRequest
metadata runtime.ServerMetadata
err error
)
val, ok := pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := server.DeleteLinkedIdentity(ctx, &protoReq)
return msg, metadata, err
}
var filter_UserService_ListPersonalAccessTokens_0 = &utilities.DoubleArray{Encoding: map[string]int{"parent": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
func request_UserService_ListPersonalAccessTokens_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
......@@ -1298,6 +1460,86 @@ func RegisterUserServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux
}
forward_UserService_ListUserSettings_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_ListLinkedIdentities_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/ListLinkedIdentities", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/linkedIdentities"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_UserService_ListLinkedIdentities_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_ListLinkedIdentities_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_UserService_CreateLinkedIdentity_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/CreateLinkedIdentity", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/linkedIdentities"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_UserService_CreateLinkedIdentity_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_CreateLinkedIdentity_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_GetLinkedIdentity_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/GetLinkedIdentity", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/linkedIdentities/*}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_UserService_GetLinkedIdentity_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_GetLinkedIdentity_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodDelete, pattern_UserService_DeleteLinkedIdentity_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/DeleteLinkedIdentity", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/linkedIdentities/*}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_UserService_DeleteLinkedIdentity_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_DeleteLinkedIdentity_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_ListPersonalAccessTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
......@@ -1725,6 +1967,74 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux
}
forward_UserService_ListUserSettings_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_ListLinkedIdentities_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/ListLinkedIdentities", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/linkedIdentities"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_UserService_ListLinkedIdentities_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_ListLinkedIdentities_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_UserService_CreateLinkedIdentity_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/CreateLinkedIdentity", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/linkedIdentities"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_UserService_CreateLinkedIdentity_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_CreateLinkedIdentity_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_GetLinkedIdentity_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/GetLinkedIdentity", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/linkedIdentities/*}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_UserService_GetLinkedIdentity_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_GetLinkedIdentity_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodDelete, pattern_UserService_DeleteLinkedIdentity_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/DeleteLinkedIdentity", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/linkedIdentities/*}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_UserService_DeleteLinkedIdentity_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_DeleteLinkedIdentity_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_ListPersonalAccessTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
......@@ -1910,6 +2220,10 @@ var (
pattern_UserService_GetUserSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "settings", "name"}, ""))
pattern_UserService_UpdateUserSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "settings", "setting.name"}, ""))
pattern_UserService_ListUserSettings_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "settings"}, ""))
pattern_UserService_ListLinkedIdentities_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "linkedIdentities"}, ""))
pattern_UserService_CreateLinkedIdentity_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "linkedIdentities"}, ""))
pattern_UserService_GetLinkedIdentity_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "linkedIdentities", "name"}, ""))
pattern_UserService_DeleteLinkedIdentity_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "linkedIdentities", "name"}, ""))
pattern_UserService_ListPersonalAccessTokens_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "personalAccessTokens"}, ""))
pattern_UserService_CreatePersonalAccessToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "personalAccessTokens"}, ""))
pattern_UserService_DeletePersonalAccessToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "personalAccessTokens", "name"}, ""))
......@@ -1934,6 +2248,10 @@ var (
forward_UserService_GetUserSetting_0 = runtime.ForwardResponseMessage
forward_UserService_UpdateUserSetting_0 = runtime.ForwardResponseMessage
forward_UserService_ListUserSettings_0 = runtime.ForwardResponseMessage
forward_UserService_ListLinkedIdentities_0 = runtime.ForwardResponseMessage
forward_UserService_CreateLinkedIdentity_0 = runtime.ForwardResponseMessage
forward_UserService_GetLinkedIdentity_0 = runtime.ForwardResponseMessage
forward_UserService_DeleteLinkedIdentity_0 = runtime.ForwardResponseMessage
forward_UserService_ListPersonalAccessTokens_0 = runtime.ForwardResponseMessage
forward_UserService_CreatePersonalAccessToken_0 = runtime.ForwardResponseMessage
forward_UserService_DeletePersonalAccessToken_0 = runtime.ForwardResponseMessage
......
......@@ -31,6 +31,10 @@ const (
UserService_GetUserSetting_FullMethodName = "/memos.api.v1.UserService/GetUserSetting"
UserService_UpdateUserSetting_FullMethodName = "/memos.api.v1.UserService/UpdateUserSetting"
UserService_ListUserSettings_FullMethodName = "/memos.api.v1.UserService/ListUserSettings"
UserService_ListLinkedIdentities_FullMethodName = "/memos.api.v1.UserService/ListLinkedIdentities"
UserService_CreateLinkedIdentity_FullMethodName = "/memos.api.v1.UserService/CreateLinkedIdentity"
UserService_GetLinkedIdentity_FullMethodName = "/memos.api.v1.UserService/GetLinkedIdentity"
UserService_DeleteLinkedIdentity_FullMethodName = "/memos.api.v1.UserService/DeleteLinkedIdentity"
UserService_ListPersonalAccessTokens_FullMethodName = "/memos.api.v1.UserService/ListPersonalAccessTokens"
UserService_CreatePersonalAccessToken_FullMethodName = "/memos.api.v1.UserService/CreatePersonalAccessToken"
UserService_DeletePersonalAccessToken_FullMethodName = "/memos.api.v1.UserService/DeletePersonalAccessToken"
......@@ -70,6 +74,14 @@ type UserServiceClient interface {
UpdateUserSetting(ctx context.Context, in *UpdateUserSettingRequest, opts ...grpc.CallOption) (*UserSetting, error)
// ListUserSettings returns a list of user settings.
ListUserSettings(ctx context.Context, in *ListUserSettingsRequest, opts ...grpc.CallOption) (*ListUserSettingsResponse, error)
// ListLinkedIdentities returns a list of linked SSO identities for a user.
ListLinkedIdentities(ctx context.Context, in *ListLinkedIdentitiesRequest, opts ...grpc.CallOption) (*ListLinkedIdentitiesResponse, error)
// CreateLinkedIdentity links an SSO identity to the authenticated user.
CreateLinkedIdentity(ctx context.Context, in *CreateLinkedIdentityRequest, opts ...grpc.CallOption) (*LinkedIdentity, error)
// GetLinkedIdentity gets a linked SSO identity for a user.
GetLinkedIdentity(ctx context.Context, in *GetLinkedIdentityRequest, opts ...grpc.CallOption) (*LinkedIdentity, error)
// DeleteLinkedIdentity unlinks an SSO identity from a user.
DeleteLinkedIdentity(ctx context.Context, in *DeleteLinkedIdentityRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
ListPersonalAccessTokens(ctx context.Context, in *ListPersonalAccessTokensRequest, opts ...grpc.CallOption) (*ListPersonalAccessTokensResponse, error)
......@@ -212,6 +224,46 @@ func (c *userServiceClient) ListUserSettings(ctx context.Context, in *ListUserSe
return out, nil
}
func (c *userServiceClient) ListLinkedIdentities(ctx context.Context, in *ListLinkedIdentitiesRequest, opts ...grpc.CallOption) (*ListLinkedIdentitiesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListLinkedIdentitiesResponse)
err := c.cc.Invoke(ctx, UserService_ListLinkedIdentities_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) CreateLinkedIdentity(ctx context.Context, in *CreateLinkedIdentityRequest, opts ...grpc.CallOption) (*LinkedIdentity, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(LinkedIdentity)
err := c.cc.Invoke(ctx, UserService_CreateLinkedIdentity_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) GetLinkedIdentity(ctx context.Context, in *GetLinkedIdentityRequest, opts ...grpc.CallOption) (*LinkedIdentity, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(LinkedIdentity)
err := c.cc.Invoke(ctx, UserService_GetLinkedIdentity_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) DeleteLinkedIdentity(ctx context.Context, in *DeleteLinkedIdentityRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, UserService_DeleteLinkedIdentity_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) ListPersonalAccessTokens(ctx context.Context, in *ListPersonalAccessTokensRequest, opts ...grpc.CallOption) (*ListPersonalAccessTokensResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListPersonalAccessTokensResponse)
......@@ -339,6 +391,14 @@ type UserServiceServer interface {
UpdateUserSetting(context.Context, *UpdateUserSettingRequest) (*UserSetting, error)
// ListUserSettings returns a list of user settings.
ListUserSettings(context.Context, *ListUserSettingsRequest) (*ListUserSettingsResponse, error)
// ListLinkedIdentities returns a list of linked SSO identities for a user.
ListLinkedIdentities(context.Context, *ListLinkedIdentitiesRequest) (*ListLinkedIdentitiesResponse, error)
// CreateLinkedIdentity links an SSO identity to the authenticated user.
CreateLinkedIdentity(context.Context, *CreateLinkedIdentityRequest) (*LinkedIdentity, error)
// GetLinkedIdentity gets a linked SSO identity for a user.
GetLinkedIdentity(context.Context, *GetLinkedIdentityRequest) (*LinkedIdentity, error)
// DeleteLinkedIdentity unlinks an SSO identity from a user.
DeleteLinkedIdentity(context.Context, *DeleteLinkedIdentityRequest) (*emptypb.Empty, error)
// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
ListPersonalAccessTokens(context.Context, *ListPersonalAccessTokensRequest) (*ListPersonalAccessTokensResponse, error)
......@@ -404,6 +464,18 @@ func (UnimplementedUserServiceServer) UpdateUserSetting(context.Context, *Update
func (UnimplementedUserServiceServer) ListUserSettings(context.Context, *ListUserSettingsRequest) (*ListUserSettingsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListUserSettings not implemented")
}
func (UnimplementedUserServiceServer) ListLinkedIdentities(context.Context, *ListLinkedIdentitiesRequest) (*ListLinkedIdentitiesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListLinkedIdentities not implemented")
}
func (UnimplementedUserServiceServer) CreateLinkedIdentity(context.Context, *CreateLinkedIdentityRequest) (*LinkedIdentity, error) {
return nil, status.Error(codes.Unimplemented, "method CreateLinkedIdentity not implemented")
}
func (UnimplementedUserServiceServer) GetLinkedIdentity(context.Context, *GetLinkedIdentityRequest) (*LinkedIdentity, error) {
return nil, status.Error(codes.Unimplemented, "method GetLinkedIdentity not implemented")
}
func (UnimplementedUserServiceServer) DeleteLinkedIdentity(context.Context, *DeleteLinkedIdentityRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteLinkedIdentity not implemented")
}
func (UnimplementedUserServiceServer) ListPersonalAccessTokens(context.Context, *ListPersonalAccessTokensRequest) (*ListPersonalAccessTokensResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListPersonalAccessTokens not implemented")
}
......@@ -653,6 +725,78 @@ func _UserService_ListUserSettings_Handler(srv interface{}, ctx context.Context,
return interceptor(ctx, in, info, handler)
}
func _UserService_ListLinkedIdentities_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListLinkedIdentitiesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).ListLinkedIdentities(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_ListLinkedIdentities_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).ListLinkedIdentities(ctx, req.(*ListLinkedIdentitiesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_CreateLinkedIdentity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateLinkedIdentityRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).CreateLinkedIdentity(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_CreateLinkedIdentity_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).CreateLinkedIdentity(ctx, req.(*CreateLinkedIdentityRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_GetLinkedIdentity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetLinkedIdentityRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).GetLinkedIdentity(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_GetLinkedIdentity_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).GetLinkedIdentity(ctx, req.(*GetLinkedIdentityRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_DeleteLinkedIdentity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteLinkedIdentityRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).DeleteLinkedIdentity(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_DeleteLinkedIdentity_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).DeleteLinkedIdentity(ctx, req.(*DeleteLinkedIdentityRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_ListPersonalAccessTokens_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListPersonalAccessTokensRequest)
if err := dec(in); err != nil {
......@@ -884,6 +1028,22 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListUserSettings",
Handler: _UserService_ListUserSettings_Handler,
},
{
MethodName: "ListLinkedIdentities",
Handler: _UserService_ListLinkedIdentities_Handler,
},
{
MethodName: "CreateLinkedIdentity",
Handler: _UserService_CreateLinkedIdentity_Handler,
},
{
MethodName: "GetLinkedIdentity",
Handler: _UserService_GetLinkedIdentity_Handler,
},
{
MethodName: "DeleteLinkedIdentity",
Handler: _UserService_DeleteLinkedIdentity_Handler,
},
{
MethodName: "ListPersonalAccessTokens",
Handler: _UserService_ListPersonalAccessTokens_Handler,
......
......@@ -1353,6 +1353,123 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/users/{user}/linkedIdentities:
get:
tags:
- UserService
description: ListLinkedIdentities returns a list of linked SSO identities for a user.
operationId: UserService_ListLinkedIdentities
parameters:
- name: user
in: path
description: The user id.
required: true
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ListLinkedIdentitiesResponse'
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
post:
tags:
- UserService
description: CreateLinkedIdentity links an SSO identity to the authenticated user.
operationId: UserService_CreateLinkedIdentity
parameters:
- name: user
in: path
description: The user id.
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreateLinkedIdentityRequest'
required: true
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/LinkedIdentity'
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/users/{user}/linkedIdentities/{linkedIdentity}:
get:
tags:
- UserService
description: GetLinkedIdentity gets a linked SSO identity for a user.
operationId: UserService_GetLinkedIdentity
parameters:
- name: user
in: path
description: The user id.
required: true
schema:
type: string
- name: linkedIdentity
in: path
description: The linkedIdentity id.
required: true
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/LinkedIdentity'
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
delete:
tags:
- UserService
description: DeleteLinkedIdentity unlinks an SSO identity from a user.
operationId: UserService_DeleteLinkedIdentity
parameters:
- name: user
in: path
description: The user id.
required: true
schema:
type: string
- name: linkedIdentity
in: path
description: The linkedIdentity id.
required: true
schema:
type: string
responses:
"200":
description: OK
content: {}
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/users/{user}/notifications:
get:
tags:
......@@ -2269,6 +2386,33 @@ components:
};
// ...
CreateLinkedIdentityRequest:
required:
- parent
- idpName
- code
- redirectUri
type: object
properties:
parent:
type: string
description: |-
Required. The parent user who owns the linked identity.
Format: users/{user}
idpName:
type: string
description: |-
Required. The identity provider to link.
Format: identity-providers/{uid}
code:
type: string
description: Required. The authorization code from the identity provider.
redirectUri:
type: string
description: Required. The redirect URI used in the OAuth flow.
codeVerifier:
type: string
description: Optional. The PKCE code verifier used in the OAuth flow.
CreatePersonalAccessTokenRequest:
required:
- parent
......@@ -2555,6 +2699,25 @@ components:
so a single entry like "project/.*" matches all tags under that prefix.
Exact tag names are also valid (they are trivially valid regex patterns).
description: Tag metadata configuration.
LinkedIdentity:
type: object
properties:
name:
type: string
description: |-
The resource name of the linked identity.
Format: users/{user}/linkedIdentities/{linked_identity}
idpName:
readOnly: true
type: string
description: |-
The resource name of the identity provider.
Format: identity-providers/{uid}
externUid:
readOnly: true
type: string
description: The external user identifier from the identity provider.
description: LinkedIdentity represents an SSO identity linked to a user account.
ListAllUserStatsResponse:
type: object
properties:
......@@ -2588,6 +2751,14 @@ components:
items:
$ref: '#/components/schemas/IdentityProvider'
description: The list of identity providers.
ListLinkedIdentitiesResponse:
type: object
properties:
linkedIdentities:
type: array
items:
$ref: '#/components/schemas/LinkedIdentity'
description: The list of linked identities.
ListMemoAttachmentsResponse:
type: object
properties:
......
......@@ -90,86 +90,13 @@ func (s *APIV1Service) SignIn(ctx context.Context, request *v1pb.SignInRequest)
existingUser = user
} else if ssoCredentials := request.GetSsoCredentials(); ssoCredentials != nil {
// Authentication Method 2: SSO (OAuth2) authentication
idpUID, err := ExtractIdentityProviderUIDFromName(ssoCredentials.IdpName)
identityProvider, userInfo, err := s.resolveSSOIdentity(ctx, ssoCredentials.IdpName, ssoCredentials.Code, ssoCredentials.RedirectUri, ssoCredentials.CodeVerifier)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid identity provider name: %v", err)
return nil, err
}
identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{
UID: &idpUID,
})
user, err := s.resolveSSOUser(ctx, nil, identityProvider, userInfo)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get identity provider, error: %v", err)
}
if identityProvider == nil {
return nil, status.Errorf(codes.InvalidArgument, "identity provider not found")
}
var userInfo *idp.IdentityProviderUserInfo
if identityProvider.Type == storepb.IdentityProvider_OAUTH2 {
oauth2IdentityProvider, err := oauth2.NewIdentityProvider(identityProvider.Config.GetOauth2Config())
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create oauth2 identity provider, error: %v", err)
}
// Pass code_verifier for PKCE support (empty string if not provided for backward compatibility)
token, err := oauth2IdentityProvider.ExchangeToken(ctx, ssoCredentials.RedirectUri, ssoCredentials.Code, ssoCredentials.CodeVerifier)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to exchange token, error: %v", err)
}
userInfo, err = oauth2IdentityProvider.UserInfo(token)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user info, error: %v", err)
}
}
identifierFilter := identityProvider.IdentifierFilter
if identifierFilter != "" {
identifierFilterRegex, err := regexp.Compile(identifierFilter)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to compile identifier filter regex, error: %v", err)
}
if !identifierFilterRegex.MatchString(userInfo.Identifier) {
return nil, status.Errorf(codes.PermissionDenied, "identifier %s is not allowed", userInfo.Identifier)
}
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
Username: &userInfo.Identifier,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user, error: %v", err)
}
if user == nil {
// Check if the user is allowed to sign up.
instanceGeneralSetting, err := s.Store.GetInstanceGeneralSetting(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get instance general setting, error: %v", err)
}
if instanceGeneralSetting.DisallowUserRegistration {
return nil, status.Errorf(codes.PermissionDenied, "user registration is not allowed")
}
// Create a new user with the user info from the identity provider.
userCreate := &store.User{
Username: userInfo.Identifier,
// The new signup user should be normal user by default.
Role: store.RoleUser,
Nickname: userInfo.DisplayName,
Email: userInfo.Email,
AvatarURL: userInfo.AvatarURL,
}
password, err := util.RandomString(20)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to generate random password, error: %v", err)
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to generate password hash, error: %v", err)
}
userCreate.PasswordHash = string(passwordHash)
user, err = s.Store.CreateUser(ctx, userCreate)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create user, error: %v", err)
}
return nil, err
}
existingUser = user
}
......@@ -193,6 +120,238 @@ func (s *APIV1Service) SignIn(ctx context.Context, request *v1pb.SignInRequest)
}, nil
}
// resolveSSOUser resolves a local user from an external-identity subject, creating the
// linkage record (and a new local user if necessary) when first login is allowed.
//
// Lookup goes through the user_identity table so that userInfo.Identifier is never used
// as the local username key. On the miss path, a local user is created with a
// UUID-based local username (see deriveSSOUsername) and the (provider, extern_uid)
// linkage is inserted in the same flow. When currentUser is provided by a caller
// outside AuthService.SignIn, the lookup miss path binds the external identity to
// that existing user instead. If the linkage insert loses a race on the unique
// (provider, extern_uid) constraint, the winning linkage's user is loaded and
// checked against the current user.
func (s *APIV1Service) resolveSSOUser(ctx context.Context, currentUser *store.User, identityProvider *storepb.IdentityProvider, userInfo *idp.IdentityProviderUserInfo) (*store.User, error) {
provider := identityProvider.Uid
externUID := userInfo.Identifier
user, err := s.getLinkedSSOUser(ctx, provider, externUID)
if err != nil {
return nil, err
}
if user != nil {
if currentUser != nil && currentUser.ID != user.ID {
return nil, status.Errorf(codes.AlreadyExists, "identity provider account is already linked to another user")
}
return user, nil
}
if currentUser != nil {
return s.bindSSOIdentityToUser(ctx, currentUser, provider, externUID)
}
// Miss path: enforce the registration gate before creating anything.
instanceGeneralSetting, err := s.Store.GetInstanceGeneralSetting(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get instance general setting, error: %v", err)
}
if instanceGeneralSetting.DisallowUserRegistration {
return nil, status.Errorf(codes.PermissionDenied, "user registration is not allowed")
}
password, err := util.RandomString(20)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to generate random password, error: %v", err)
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to generate password hash, error: %v", err)
}
username, err := deriveSSOUsername()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to derive username, error: %v", err)
}
user, err = s.Store.CreateUser(ctx, &store.User{
Username: username,
Role: store.RoleUser,
Nickname: userInfo.DisplayName,
Email: userInfo.Email,
AvatarURL: userInfo.AvatarURL,
PasswordHash: string(passwordHash),
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create user, error: %v", err)
}
if _, err := s.Store.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: provider,
ExternUID: externUID,
}); err != nil {
// Best-effort cleanup: the provisional user row has no linkage and should not remain.
_ = s.Store.DeleteUser(ctx, &store.DeleteUser{ID: user.ID})
if isUniqueConstraintViolation(err) {
// Concurrent first login won the race; load the winning linkage's user.
winner, getErr := s.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
Provider: &provider,
ExternUID: &externUID,
})
if getErr != nil {
return nil, status.Errorf(codes.Internal, "failed to reload user identity after race, error: %v", getErr)
}
if winner == nil {
return nil, status.Errorf(codes.Internal, "user identity conflict reported but no winning row found")
}
winnerUser, getErr := s.Store.GetUser(ctx, &store.FindUser{ID: &winner.UserID})
if getErr != nil {
return nil, status.Errorf(codes.Internal, "failed to get user after race, error: %v", getErr)
}
if winnerUser == nil {
return nil, status.Errorf(codes.Internal, "linked user %d not found after race", winner.UserID)
}
return winnerUser, nil
}
return nil, status.Errorf(codes.Internal, "failed to create user identity, error: %v", err)
}
return user, nil
}
func (s *APIV1Service) resolveSSOIdentity(ctx context.Context, idpName, code, redirectURI, codeVerifier string) (*storepb.IdentityProvider, *idp.IdentityProviderUserInfo, error) {
idpUID, err := ExtractIdentityProviderUIDFromName(idpName)
if err != nil {
return nil, nil, status.Errorf(codes.InvalidArgument, "invalid identity provider name: %v", err)
}
identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{
UID: &idpUID,
})
if err != nil {
return nil, nil, status.Errorf(codes.Internal, "failed to get identity provider, error: %v", err)
}
if identityProvider == nil {
return nil, nil, status.Errorf(codes.InvalidArgument, "identity provider not found")
}
var userInfo *idp.IdentityProviderUserInfo
if identityProvider.Type == storepb.IdentityProvider_OAUTH2 {
oauth2IdentityProvider, err := oauth2.NewIdentityProvider(identityProvider.Config.GetOauth2Config())
if err != nil {
return nil, nil, status.Errorf(codes.Internal, "failed to create oauth2 identity provider, error: %v", err)
}
// Pass code_verifier for PKCE support (empty string if not provided for backward compatibility)
token, err := oauth2IdentityProvider.ExchangeToken(ctx, redirectURI, code, codeVerifier)
if err != nil {
return nil, nil, status.Errorf(codes.Internal, "failed to exchange token, error: %v", err)
}
userInfo, err = oauth2IdentityProvider.UserInfo(token)
if err != nil {
return nil, nil, status.Errorf(codes.Internal, "failed to get user info, error: %v", err)
}
}
identifierFilter := identityProvider.IdentifierFilter
if identifierFilter != "" {
identifierFilterRegex, err := regexp.Compile(identifierFilter)
if err != nil {
return nil, nil, status.Errorf(codes.Internal, "failed to compile identifier filter regex, error: %v", err)
}
if !identifierFilterRegex.MatchString(userInfo.Identifier) {
return nil, nil, status.Errorf(codes.PermissionDenied, "identifier %s is not allowed", userInfo.Identifier)
}
}
return identityProvider, userInfo, nil
}
func (s *APIV1Service) getLinkedSSOUser(ctx context.Context, provider, externUID string) (*store.User, error) {
identity, err := s.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
Provider: &provider,
ExternUID: &externUID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user identity, error: %v", err)
}
if identity == nil {
return nil, nil
}
user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &identity.UserID})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user, error: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.Internal, "linked user %d not found for identity %d", identity.UserID, identity.ID)
}
return user, nil
}
func (s *APIV1Service) bindSSOIdentityToUser(ctx context.Context, currentUser *store.User, provider, externUID string) (*store.User, error) {
existingForProvider, err := s.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
UserID: &currentUser.ID,
Provider: &provider,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get existing linked identity, error: %v", err)
}
if existingForProvider != nil {
if existingForProvider.ExternUID == externUID {
return currentUser, nil
}
return nil, status.Errorf(codes.AlreadyExists, "identity provider is already linked to another external account for this user")
}
if _, err := s.Store.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: currentUser.ID,
Provider: provider,
ExternUID: externUID,
}); err != nil {
if isUniqueConstraintViolation(err) {
winner, getErr := s.getLinkedSSOUser(ctx, provider, externUID)
if getErr != nil {
return nil, getErr
}
if winner != nil {
if winner.ID != currentUser.ID {
return nil, status.Errorf(codes.AlreadyExists, "identity provider account is already linked to another user")
}
return currentUser, nil
}
existingForProvider, getErr := s.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
UserID: &currentUser.ID,
Provider: &provider,
})
if getErr != nil {
return nil, status.Errorf(codes.Internal, "failed to reload linked identity after race, error: %v", getErr)
}
if existingForProvider != nil {
if existingForProvider.ExternUID == externUID {
return currentUser, nil
}
return nil, status.Errorf(codes.AlreadyExists, "identity provider is already linked to another external account for this user")
}
return nil, status.Errorf(codes.Internal, "user identity conflict reported but no winning row found")
}
return nil, status.Errorf(codes.Internal, "failed to create user identity, error: %v", err)
}
return currentUser, nil
}
// isUniqueConstraintViolation matches the driver-specific error messages that each
// supported backend emits when any UNIQUE constraint rejects an insert. Callers
// disambiguate which constraint was hit from the insertion context (e.g. inserting
// a user_identity row can only violate UNIQUE(provider, extern_uid); inserting a
// user row can only violate UNIQUE(username)). Matches the pattern used in
// memo_service.go for the memo UID unique check.
func isUniqueConstraintViolation(err error) bool {
if err == nil {
return false
}
msg := err.Error()
return strings.Contains(msg, "UNIQUE constraint failed") ||
strings.Contains(msg, "duplicate key") ||
strings.Contains(msg, "Duplicate entry")
}
// doSignIn performs the actual sign-in operation by creating a session and setting the cookie.
//
// This function:
......
......@@ -112,11 +112,9 @@ func (s *ConnectServiceHandler) UpdateUser(ctx context.Context, req *connect.Req
}
func (s *ConnectServiceHandler) DeleteUser(ctx context.Context, req *connect.Request[v1pb.DeleteUserRequest]) (*connect.Response[emptypb.Empty], error) {
resp, err := s.APIV1Service.DeleteUser(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}
return connect.NewResponse(resp), nil
return connectWithHeaderCarrier(ctx, func(ctx context.Context) (*emptypb.Empty, error) {
return s.APIV1Service.DeleteUser(ctx, req.Msg)
})
}
func (s *ConnectServiceHandler) ListAllUserStats(ctx context.Context, req *connect.Request[v1pb.ListAllUserStatsRequest]) (*connect.Response[v1pb.ListAllUserStatsResponse], error) {
......@@ -159,6 +157,38 @@ func (s *ConnectServiceHandler) ListUserSettings(ctx context.Context, req *conne
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) ListLinkedIdentities(ctx context.Context, req *connect.Request[v1pb.ListLinkedIdentitiesRequest]) (*connect.Response[v1pb.ListLinkedIdentitiesResponse], error) {
resp, err := s.APIV1Service.ListLinkedIdentities(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) CreateLinkedIdentity(ctx context.Context, req *connect.Request[v1pb.CreateLinkedIdentityRequest]) (*connect.Response[v1pb.LinkedIdentity], error) {
resp, err := s.APIV1Service.CreateLinkedIdentity(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) GetLinkedIdentity(ctx context.Context, req *connect.Request[v1pb.GetLinkedIdentityRequest]) (*connect.Response[v1pb.LinkedIdentity], error) {
resp, err := s.APIV1Service.GetLinkedIdentity(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) DeleteLinkedIdentity(ctx context.Context, req *connect.Request[v1pb.DeleteLinkedIdentityRequest]) (*connect.Response[emptypb.Empty], error) {
resp, err := s.APIV1Service.DeleteLinkedIdentity(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) ListPersonalAccessTokens(ctx context.Context, req *connect.Request[v1pb.ListPersonalAccessTokensRequest]) (*connect.Response[v1pb.ListPersonalAccessTokensResponse], error) {
resp, err := s.APIV1Service.ListPersonalAccessTokens(ctx, req.Msg)
if err != nil {
......
package v1
import (
"github.com/pkg/errors"
"github.com/usememos/memos/internal/util"
)
// deriveSSOUsername produces the local username for a new SSO-created user.
//
// The current policy is to use a standard UUID string directly. This keeps the
// username independent of IdP profile fields and avoids availability probes or
// retry loops around concurrent first-time logins.
func deriveSSOUsername() (string, error) {
username := util.GenUUID()
if err := validateUsername(username); err != nil {
return "", errors.Wrap(err, "generated UUID did not satisfy username constraints")
}
return username, nil
}
package test
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
storepb "github.com/usememos/memos/proto/gen/store"
apiv1 "github.com/usememos/memos/server/router/api/v1"
"github.com/usememos/memos/store"
)
func TestCreateLinkedIdentityBindsCurrentUser(t *testing.T) {
t.Parallel()
ts := NewTestService(t)
defer ts.Cleanup()
ctx := context.Background()
currentUser, err := ts.CreateRegularUser(ctx, "alice")
require.NoError(t, err)
mockIDP := newMockOAuthServer(t, "bind-code", "bind-access-token", map[string]any{
"sub": "google-sub-1",
"name": "Alice Example",
"email": "alice@example.com",
})
defer mockIDP.Close()
idpName := createTestingOAuthIdentityProvider(ctx, t, ts, mockIDP.URL, "google-bind")
beforeUsers, err := ts.Store.ListUsers(ctx, &store.FindUser{})
require.NoError(t, err)
authCtx := ts.CreateUserContext(apiv1.WithHeaderCarrier(ctx), currentUser.ID)
response, err := ts.Service.CreateLinkedIdentity(authCtx, &v1pb.CreateLinkedIdentityRequest{
Parent: apiv1.BuildUserName(currentUser.Username),
IdpName: idpName,
Code: "bind-code",
RedirectUri: "http://localhost:8080/auth/callback",
})
require.NoError(t, err)
require.NotNil(t, response)
require.Equal(t, apiv1.BuildUserName(currentUser.Username)+"/linkedIdentities/google-bind", response.Name)
require.Equal(t, apiv1.IdentityProviderNamePrefix+"google-bind", response.IdpName)
require.Equal(t, "google-sub-1", response.ExternUid)
afterUsers, err := ts.Store.ListUsers(ctx, &store.FindUser{})
require.NoError(t, err)
require.Len(t, afterUsers, len(beforeUsers))
provider := "google-bind"
externUID := "google-sub-1"
identity, err := ts.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
Provider: &provider,
ExternUID: &externUID,
})
require.NoError(t, err)
require.NotNil(t, identity)
require.Equal(t, currentUser.ID, identity.UserID)
}
func TestCreateLinkedIdentityRejectsBindingIdentityLinkedToAnotherUser(t *testing.T) {
t.Parallel()
ts := NewTestService(t)
defer ts.Cleanup()
ctx := context.Background()
owner, err := ts.CreateRegularUser(ctx, "owner")
require.NoError(t, err)
binder, err := ts.CreateRegularUser(ctx, "binder")
require.NoError(t, err)
mockIDP := newMockOAuthServer(t, "conflict-code", "conflict-access-token", map[string]any{
"sub": "google-sub-2",
"name": "Conflict Example",
"email": "conflict@example.com",
})
defer mockIDP.Close()
idpName := createTestingOAuthIdentityProvider(ctx, t, ts, mockIDP.URL, "google-conflict")
_, err = ts.Store.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: owner.ID,
Provider: "google-conflict",
ExternUID: "google-sub-2",
})
require.NoError(t, err)
authCtx := ts.CreateUserContext(apiv1.WithHeaderCarrier(ctx), binder.ID)
_, err = ts.Service.CreateLinkedIdentity(authCtx, &v1pb.CreateLinkedIdentityRequest{
Parent: apiv1.BuildUserName(binder.Username),
IdpName: idpName,
Code: "conflict-code",
RedirectUri: "http://localhost:8080/auth/callback",
})
require.Error(t, err)
require.Equal(t, codes.AlreadyExists, status.Code(err))
}
func TestListAndDeleteLinkedIdentities(t *testing.T) {
t.Parallel()
ts := NewTestService(t)
defer ts.Cleanup()
ctx := context.Background()
currentUser, err := ts.CreateRegularUser(ctx, "alice")
require.NoError(t, err)
_, err = ts.Store.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: currentUser.ID,
Provider: "google",
ExternUID: "alice@gmail.com",
})
require.NoError(t, err)
authCtx := ts.CreateUserContext(ctx, currentUser.ID)
listResp, err := ts.Service.ListLinkedIdentities(authCtx, &v1pb.ListLinkedIdentitiesRequest{
Parent: apiv1.BuildUserName(currentUser.Username),
})
require.NoError(t, err)
require.Len(t, listResp.LinkedIdentities, 1)
linkedIdentityName := apiv1.BuildUserName(currentUser.Username) + "/linkedIdentities/google"
require.Equal(t, linkedIdentityName, listResp.LinkedIdentities[0].Name)
require.Equal(t, apiv1.IdentityProviderNamePrefix+"google", listResp.LinkedIdentities[0].IdpName)
require.Equal(t, "alice@gmail.com", listResp.LinkedIdentities[0].ExternUid)
got, err := ts.Service.GetLinkedIdentity(authCtx, &v1pb.GetLinkedIdentityRequest{
Name: linkedIdentityName,
})
require.NoError(t, err)
require.Equal(t, linkedIdentityName, got.Name)
require.Equal(t, apiv1.IdentityProviderNamePrefix+"google", got.IdpName)
require.Equal(t, "alice@gmail.com", got.ExternUid)
_, err = ts.Service.DeleteLinkedIdentity(authCtx, &v1pb.DeleteLinkedIdentityRequest{
Name: linkedIdentityName,
})
require.NoError(t, err)
listResp, err = ts.Service.ListLinkedIdentities(authCtx, &v1pb.ListLinkedIdentitiesRequest{
Parent: apiv1.BuildUserName(currentUser.Username),
})
require.NoError(t, err)
require.Empty(t, listResp.LinkedIdentities)
}
func TestCreateLinkedIdentityRejectsSecondIdentityForSameProvider(t *testing.T) {
t.Parallel()
ts := NewTestService(t)
defer ts.Cleanup()
ctx := context.Background()
currentUser, err := ts.CreateRegularUser(ctx, "alice")
require.NoError(t, err)
_, err = ts.Store.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: currentUser.ID,
Provider: "google-provider",
ExternUID: "google-sub-1",
})
require.NoError(t, err)
mockIDP := newMockOAuthServer(t, "second-code", "second-access-token", map[string]any{
"sub": "google-sub-2",
"name": "Alice Example",
"email": "alice@example.com",
})
defer mockIDP.Close()
idpName := createTestingOAuthIdentityProvider(ctx, t, ts, mockIDP.URL, "google-provider")
authCtx := ts.CreateUserContext(apiv1.WithHeaderCarrier(ctx), currentUser.ID)
_, err = ts.Service.CreateLinkedIdentity(authCtx, &v1pb.CreateLinkedIdentityRequest{
Parent: apiv1.BuildUserName(currentUser.Username),
IdpName: idpName,
Code: "second-code",
RedirectUri: "http://localhost:8080/auth/callback",
})
require.Error(t, err)
require.Equal(t, codes.AlreadyExists, status.Code(err))
}
func createTestingOAuthIdentityProvider(ctx context.Context, t *testing.T, ts *TestService, serverURL, uid string) string {
t.Helper()
idp, err := ts.Store.CreateIdentityProvider(ctx, &storepb.IdentityProvider{
Uid: uid,
Name: "Google",
Type: storepb.IdentityProvider_OAUTH2,
Config: &storepb.IdentityProviderConfig{
Config: &storepb.IdentityProviderConfig_Oauth2Config{
Oauth2Config: &storepb.OAuth2Config{
ClientId: "test-client-id",
ClientSecret: "test-client-secret",
AuthUrl: serverURL + "/oauth2/authorize",
TokenUrl: serverURL + "/oauth2/token",
UserInfoUrl: serverURL + "/oauth2/userinfo",
FieldMapping: &storepb.FieldMapping{
Identifier: "sub",
DisplayName: "name",
Email: "email",
},
},
},
},
})
require.NoError(t, err)
return apiv1.IdentityProviderNamePrefix + idp.Uid
}
func newMockOAuthServer(t *testing.T, code, accessToken string, userInfo map[string]any) *httptest.Server {
t.Helper()
userInfoBytes, err := json.Marshal(userInfo)
require.NoError(t, err)
mux := http.NewServeMux()
mux.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
values, err := url.ParseQuery(string(body))
require.NoError(t, err)
require.Equal(t, code, values.Get("code"))
require.Equal(t, "authorization_code", values.Get("grant_type"))
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(map[string]any{
"access_token": accessToken,
"token_type": "Bearer",
"expires_in": 3600,
})
require.NoError(t, err)
})
mux.HandleFunc("/oauth2/userinfo", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, err := w.Write(userInfoBytes)
require.NoError(t, err)
})
return httptest.NewServer(mux)
}
package test
import (
"context"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
storepb "github.com/usememos/memos/proto/gen/store"
apiv1 "github.com/usememos/memos/server/router/api/v1"
"github.com/usememos/memos/store"
)
func TestDeleteUserSelfDeleteCleansAccountDataAndAuthCookies(t *testing.T) {
t.Parallel()
ts := NewTestService(t)
defer ts.Cleanup()
ctx := context.Background()
user, err := ts.CreateRegularUser(ctx, "alice")
require.NoError(t, err)
_, err = ts.Store.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "google",
ExternUID: "alice-google-sub",
})
require.NoError(t, err)
err = ts.Store.AddUserRefreshToken(ctx, user.ID, &storepb.RefreshTokensUserSetting_RefreshToken{
TokenId: "refresh-token-id",
ExpiresAt: timestamppb.New(time.Now().Add(time.Hour)),
CreatedAt: timestamppb.Now(),
})
require.NoError(t, err)
headerCtx := apiv1.WithHeaderCarrier(ctx)
authCtx := ts.CreateUserContext(headerCtx, user.ID)
_, err = ts.Service.DeleteUser(authCtx, &v1pb.DeleteUserRequest{
Name: apiv1.BuildUserName(user.Username),
})
require.NoError(t, err)
deletedUser, err := ts.Store.GetUser(ctx, &store.FindUser{ID: &user.ID})
require.NoError(t, err)
require.Nil(t, deletedUser)
identities, err := ts.Store.ListUserIdentities(ctx, &store.FindUserIdentity{UserID: &user.ID})
require.NoError(t, err)
require.Empty(t, identities)
refreshSetting, err := ts.Store.GetUserSetting(ctx, &store.FindUserSetting{
UserID: &user.ID,
Key: storepb.UserSetting_REFRESH_TOKENS,
})
require.NoError(t, err)
require.Nil(t, refreshSetting)
carrier := apiv1.GetHeaderCarrier(authCtx)
require.NotNil(t, carrier)
require.Contains(t, strings.ToLower(carrier.Get("Set-Cookie")), "memos_refresh=")
}
......@@ -335,12 +335,29 @@ func (s *APIV1Service) DeleteUser(ctx context.Context, request *v1pb.DeleteUserR
if currentUser.ID != userID && currentUser.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
isSelfDelete := currentUser.ID == userID
if err := s.Store.DeleteUserIdentities(ctx, &store.DeleteUserIdentity{
UserID: &userID,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete user identities: %v", err)
}
if err := s.Store.DeleteUserSettings(ctx, &store.DeleteUserSetting{
UserID: &userID,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete user settings: %v", err)
}
if err := s.Store.DeleteUser(ctx, &store.DeleteUser{
ID: user.ID,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete user: %v", err)
}
if isSelfDelete {
if err := s.clearAuthCookies(ctx); err != nil {
slog.Warn("failed to clear auth cookies after self delete", "user_id", userID, "error", err)
}
}
return &emptypb.Empty{}, nil
}
......@@ -390,6 +407,27 @@ func (s *APIV1Service) resolveUserAndWebhookIDFromName(ctx context.Context, name
return user, parts[3], nil
}
func (s *APIV1Service) resolveUserAndLinkedIdentityProviderFromName(ctx context.Context, name string) (*store.User, string, error) {
parts := strings.Split(name, "/")
if len(parts) != 4 || parts[0] != "users" || parts[2] != "linkedIdentities" {
return nil, "", errors.Errorf("invalid linked identity name: %s", name)
}
user, err := s.resolveUserFromName(ctx, BuildUserName(parts[1]))
if err != nil {
return nil, "", err
}
return user, parts[3], nil
}
func convertLinkedIdentityFromStore(user *store.User, identity *store.UserIdentity) *v1pb.LinkedIdentity {
return &v1pb.LinkedIdentity{
Name: fmt.Sprintf("%s/linkedIdentities/%s", BuildUserName(user.Username), identity.Provider),
IdpName: IdentityProviderNamePrefix + identity.Provider,
ExternUid: identity.ExternUID,
}
}
func (s *APIV1Service) resolveUserAndNotificationIDFromName(ctx context.Context, name string) (*store.User, int32, error) {
parts := strings.Split(name, "/")
if len(parts) != 4 || parts[0] != "users" || parts[2] != "notifications" {
......@@ -597,6 +635,141 @@ func (s *APIV1Service) ListUserSettings(ctx context.Context, request *v1pb.ListU
return response, nil
}
func (s *APIV1Service) ListLinkedIdentities(ctx context.Context, request *v1pb.ListLinkedIdentitiesRequest) (*v1pb.ListLinkedIdentitiesResponse, error) {
user, err := s.resolveUserFromName(ctx, request.Parent)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err)
}
userID := user.ID
claims := auth.GetUserClaims(ctx)
if claims == nil || claims.UserID != userID {
currentUser, _ := s.fetchCurrentUser(ctx)
if currentUser == nil || (currentUser.ID != userID && currentUser.Role != store.RoleAdmin) {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
}
identities, err := s.Store.ListUserIdentities(ctx, &store.FindUserIdentity{UserID: &userID})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list linked identities: %v", err)
}
response := &v1pb.ListLinkedIdentitiesResponse{
LinkedIdentities: []*v1pb.LinkedIdentity{},
}
for _, identity := range identities {
response.LinkedIdentities = append(response.LinkedIdentities, convertLinkedIdentityFromStore(user, identity))
}
return response, nil
}
func (s *APIV1Service) CreateLinkedIdentity(ctx context.Context, request *v1pb.CreateLinkedIdentityRequest) (*v1pb.LinkedIdentity, error) {
user, err := s.resolveUserFromName(ctx, request.Parent)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err)
}
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
}
if currentUser.ID != user.ID {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
identityProvider, userInfo, err := s.resolveSSOIdentity(ctx, request.IdpName, request.Code, request.RedirectUri, request.CodeVerifier)
if err != nil {
return nil, err
}
provider := identityProvider.Uid
externUID := userInfo.Identifier
if _, err := s.bindSSOIdentityToUser(ctx, currentUser, provider, externUID); err != nil {
return nil, err
}
identity, err := s.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
UserID: &currentUser.ID,
Provider: &provider,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get linked identity: %v", err)
}
if identity == nil {
return nil, status.Errorf(codes.Internal, "linked identity not found after creation")
}
return convertLinkedIdentityFromStore(user, identity), nil
}
func (s *APIV1Service) GetLinkedIdentity(ctx context.Context, request *v1pb.GetLinkedIdentityRequest) (*v1pb.LinkedIdentity, error) {
user, provider, err := s.resolveUserAndLinkedIdentityProviderFromName(ctx, request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid linked identity name: %v", err)
}
userID := user.ID
claims := auth.GetUserClaims(ctx)
if claims == nil || claims.UserID != userID {
currentUser, _ := s.fetchCurrentUser(ctx)
if currentUser == nil || (currentUser.ID != userID && currentUser.Role != store.RoleAdmin) {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
}
identity, err := s.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
UserID: &userID,
Provider: &provider,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get linked identity: %v", err)
}
if identity == nil {
return nil, status.Errorf(codes.NotFound, "linked identity not found")
}
return convertLinkedIdentityFromStore(user, identity), nil
}
func (s *APIV1Service) DeleteLinkedIdentity(ctx context.Context, request *v1pb.DeleteLinkedIdentityRequest) (*emptypb.Empty, error) {
user, provider, err := s.resolveUserAndLinkedIdentityProviderFromName(ctx, request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid linked identity name: %v", err)
}
userID := user.ID
claims := auth.GetUserClaims(ctx)
if claims == nil || claims.UserID != userID {
currentUser, _ := s.fetchCurrentUser(ctx)
if currentUser == nil || (currentUser.ID != userID && currentUser.Role != store.RoleAdmin) {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
}
existing, err := s.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
UserID: &userID,
Provider: &provider,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get linked identity: %v", err)
}
if existing == nil {
return nil, status.Errorf(codes.NotFound, "linked identity not found")
}
if err := s.Store.DeleteUserIdentities(ctx, &store.DeleteUserIdentity{
UserID: &userID,
Provider: &provider,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete linked identity: %v", err)
}
return &emptypb.Empty{}, nil
}
// ListPersonalAccessTokens retrieves all Personal Access Tokens (PATs) for a user.
//
// Personal Access Tokens are used for:
......
package mysql
import (
"context"
"strings"
"github.com/pkg/errors"
"github.com/usememos/memos/store"
)
func (d *DB) CreateUserIdentity(ctx context.Context, create *store.UserIdentity) (*store.UserIdentity, error) {
stmt := "INSERT INTO `user_identity` (`user_id`, `provider`, `extern_uid`) VALUES (?, ?, ?)"
result, err := d.db.ExecContext(ctx, stmt, create.UserID, create.Provider, create.ExternUID)
if err != nil {
return nil, err
}
rawID, err := result.LastInsertId()
if err != nil {
return nil, err
}
id := int32(rawID)
list, err := d.ListUserIdentities(ctx, &store.FindUserIdentity{ID: &id})
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, errors.Errorf("failed to create user identity")
}
return list[0], nil
}
func (d *DB) ListUserIdentities(ctx context.Context, find *store.FindUserIdentity) ([]*store.UserIdentity, error) {
where, args := []string{"1 = 1"}, []any{}
if find.ID != nil {
where, args = append(where, "`id` = ?"), append(args, *find.ID)
}
if find.UserID != nil {
where, args = append(where, "`user_id` = ?"), append(args, *find.UserID)
}
if find.Provider != nil {
where, args = append(where, "`provider` = ?"), append(args, *find.Provider)
}
if find.ExternUID != nil {
where, args = append(where, "`extern_uid` = ?"), append(args, *find.ExternUID)
}
rows, err := d.db.QueryContext(ctx, `
SELECT
id,
user_id,
provider,
extern_uid,
created_ts,
updated_ts
FROM user_identity
WHERE `+strings.Join(where, " AND ")+`
ORDER BY id ASC`,
args...,
)
if err != nil {
return nil, err
}
defer rows.Close()
list := []*store.UserIdentity{}
for rows.Next() {
ui := &store.UserIdentity{}
if err := rows.Scan(
&ui.ID,
&ui.UserID,
&ui.Provider,
&ui.ExternUID,
&ui.CreatedTs,
&ui.UpdatedTs,
); err != nil {
return nil, err
}
list = append(list, ui)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
func (d *DB) DeleteUserIdentities(ctx context.Context, delete *store.DeleteUserIdentity) error {
where, args := []string{"1 = 1"}, []any{}
if delete.ID != nil {
where, args = append(where, "`id` = ?"), append(args, *delete.ID)
}
if delete.UserID != nil {
where, args = append(where, "`user_id` = ?"), append(args, *delete.UserID)
}
if delete.Provider != nil {
where, args = append(where, "`provider` = ?"), append(args, *delete.Provider)
}
if _, err := d.db.ExecContext(ctx, "DELETE FROM `user_identity` WHERE "+strings.Join(where, " AND "), args...); err != nil {
return err
}
return nil
}
......@@ -57,6 +57,22 @@ func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting)
return userSettingList, nil
}
func (d *DB) DeleteUserSettings(ctx context.Context, delete *store.DeleteUserSetting) error {
where, args := []string{"1 = 1"}, []any{}
if v := delete.Key; v != storepb.UserSetting_KEY_UNSPECIFIED {
where, args = append(where, "`key` = ?"), append(args, v.String())
}
if v := delete.UserID; v != nil {
where, args = append(where, "`user_id` = ?"), append(args, *v)
}
if _, err := d.db.ExecContext(ctx, "DELETE FROM `user_setting` WHERE "+strings.Join(where, " AND "), args...); err != nil {
return err
}
return nil
}
func (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) {
query := `
SELECT
......
package postgres
import (
"context"
"strings"
"github.com/usememos/memos/store"
)
func (d *DB) CreateUserIdentity(ctx context.Context, create *store.UserIdentity) (*store.UserIdentity, error) {
stmt := "INSERT INTO user_identity (user_id, provider, extern_uid) VALUES (" + placeholders(3) + ") RETURNING id, created_ts, updated_ts"
if err := d.db.QueryRowContext(ctx, stmt, create.UserID, create.Provider, create.ExternUID).Scan(
&create.ID,
&create.CreatedTs,
&create.UpdatedTs,
); err != nil {
return nil, err
}
return create, nil
}
func (d *DB) ListUserIdentities(ctx context.Context, find *store.FindUserIdentity) ([]*store.UserIdentity, error) {
where, args := []string{"1 = 1"}, []any{}
if find.ID != nil {
where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *find.ID)
}
if find.UserID != nil {
where, args = append(where, "user_id = "+placeholder(len(args)+1)), append(args, *find.UserID)
}
if find.Provider != nil {
where, args = append(where, "provider = "+placeholder(len(args)+1)), append(args, *find.Provider)
}
if find.ExternUID != nil {
where, args = append(where, "extern_uid = "+placeholder(len(args)+1)), append(args, *find.ExternUID)
}
rows, err := d.db.QueryContext(ctx, `
SELECT
id,
user_id,
provider,
extern_uid,
created_ts,
updated_ts
FROM user_identity
WHERE `+strings.Join(where, " AND ")+`
ORDER BY id ASC`,
args...,
)
if err != nil {
return nil, err
}
defer rows.Close()
list := []*store.UserIdentity{}
for rows.Next() {
ui := &store.UserIdentity{}
if err := rows.Scan(
&ui.ID,
&ui.UserID,
&ui.Provider,
&ui.ExternUID,
&ui.CreatedTs,
&ui.UpdatedTs,
); err != nil {
return nil, err
}
list = append(list, ui)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
func (d *DB) DeleteUserIdentities(ctx context.Context, delete *store.DeleteUserIdentity) error {
where, args := []string{"1 = 1"}, []any{}
if delete.ID != nil {
where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *delete.ID)
}
if delete.UserID != nil {
where, args = append(where, "user_id = "+placeholder(len(args)+1)), append(args, *delete.UserID)
}
if delete.Provider != nil {
where, args = append(where, "provider = "+placeholder(len(args)+1)), append(args, *delete.Provider)
}
if _, err := d.db.ExecContext(ctx, "DELETE FROM user_identity WHERE "+strings.Join(where, " AND "), args...); err != nil {
return err
}
return nil
}
......@@ -70,6 +70,22 @@ func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting)
return userSettingList, nil
}
func (d *DB) DeleteUserSettings(ctx context.Context, delete *store.DeleteUserSetting) error {
where, args := []string{"1 = 1"}, []any{}
if v := delete.Key; v != storepb.UserSetting_KEY_UNSPECIFIED {
where, args = append(where, "key = "+placeholder(len(args)+1)), append(args, v.String())
}
if v := delete.UserID; v != nil {
where, args = append(where, "user_id = "+placeholder(len(args)+1)), append(args, *v)
}
if _, err := d.db.ExecContext(ctx, "DELETE FROM user_setting WHERE "+strings.Join(where, " AND "), args...); err != nil {
return err
}
return nil
}
func (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) {
// Simplified query: fetch all PERSONAL_ACCESS_TOKENS rows and search in Go
// This matches SQLite/MySQL behavior and avoids PostgreSQL's strict JSONB errors
......
package sqlite
import (
"context"
"strings"
"github.com/usememos/memos/store"
)
func (d *DB) CreateUserIdentity(ctx context.Context, create *store.UserIdentity) (*store.UserIdentity, error) {
stmt := "INSERT INTO `user_identity` (`user_id`, `provider`, `extern_uid`) VALUES (?, ?, ?) RETURNING `id`, `created_ts`, `updated_ts`"
if err := d.db.QueryRowContext(ctx, stmt, create.UserID, create.Provider, create.ExternUID).Scan(
&create.ID,
&create.CreatedTs,
&create.UpdatedTs,
); err != nil {
return nil, err
}
return create, nil
}
func (d *DB) ListUserIdentities(ctx context.Context, find *store.FindUserIdentity) ([]*store.UserIdentity, error) {
where, args := []string{"1 = 1"}, []any{}
if find.ID != nil {
where, args = append(where, "`id` = ?"), append(args, *find.ID)
}
if find.UserID != nil {
where, args = append(where, "`user_id` = ?"), append(args, *find.UserID)
}
if find.Provider != nil {
where, args = append(where, "`provider` = ?"), append(args, *find.Provider)
}
if find.ExternUID != nil {
where, args = append(where, "`extern_uid` = ?"), append(args, *find.ExternUID)
}
rows, err := d.db.QueryContext(ctx, `
SELECT
id,
user_id,
provider,
extern_uid,
created_ts,
updated_ts
FROM user_identity
WHERE `+strings.Join(where, " AND ")+`
ORDER BY id ASC`,
args...,
)
if err != nil {
return nil, err
}
defer rows.Close()
list := []*store.UserIdentity{}
for rows.Next() {
ui := &store.UserIdentity{}
if err := rows.Scan(
&ui.ID,
&ui.UserID,
&ui.Provider,
&ui.ExternUID,
&ui.CreatedTs,
&ui.UpdatedTs,
); err != nil {
return nil, err
}
list = append(list, ui)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
func (d *DB) DeleteUserIdentities(ctx context.Context, delete *store.DeleteUserIdentity) error {
where, args := []string{"1 = 1"}, []any{}
if delete.ID != nil {
where, args = append(where, "`id` = ?"), append(args, *delete.ID)
}
if delete.UserID != nil {
where, args = append(where, "`user_id` = ?"), append(args, *delete.UserID)
}
if delete.Provider != nil {
where, args = append(where, "`provider` = ?"), append(args, *delete.Provider)
}
if _, err := d.db.ExecContext(ctx, "DELETE FROM `user_identity` WHERE "+strings.Join(where, " AND "), args...); err != nil {
return err
}
return nil
}
......@@ -69,6 +69,22 @@ func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting)
return userSettingList, nil
}
func (d *DB) DeleteUserSettings(ctx context.Context, delete *store.DeleteUserSetting) error {
where, args := []string{"1 = 1"}, []any{}
if v := delete.Key; v != storepb.UserSetting_KEY_UNSPECIFIED {
where, args = append(where, "key = ?"), append(args, v.String())
}
if v := delete.UserID; v != nil {
where, args = append(where, "user_id = ?"), append(args, *v)
}
if _, err := d.db.ExecContext(ctx, "DELETE FROM user_setting WHERE "+strings.Join(where, " AND "), args...); err != nil {
return err
}
return nil
}
func (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) {
query := `
SELECT
......
......@@ -45,6 +45,7 @@ type Driver interface {
// UserSetting model related methods.
UpsertUserSetting(ctx context.Context, upsert *UserSetting) (*UserSetting, error)
ListUserSettings(ctx context.Context, find *FindUserSetting) ([]*UserSetting, error)
DeleteUserSettings(ctx context.Context, delete *DeleteUserSetting) error
GetUserByPATHash(ctx context.Context, tokenHash string) (*PATQueryResult, error)
// IdentityProvider model related methods.
......@@ -70,4 +71,9 @@ type Driver interface {
ListMemoShares(ctx context.Context, find *FindMemoShare) ([]*MemoShare, error)
GetMemoShare(ctx context.Context, find *FindMemoShare) (*MemoShare, error)
DeleteMemoShare(ctx context.Context, delete *DeleteMemoShare) error
// UserIdentity model related methods.
CreateUserIdentity(ctx context.Context, create *UserIdentity) (*UserIdentity, error)
ListUserIdentities(ctx context.Context, find *FindUserIdentity) ([]*UserIdentity, error)
DeleteUserIdentities(ctx context.Context, delete *DeleteUserIdentity) error
}
-- user_identity stores the linkage between an external identity subject and a local user.
-- (provider, extern_uid) is unique across the table; provider stores the idp.uid.
-- Each local user can link at most one external account per provider.
CREATE TABLE `user_identity` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` INT NOT NULL,
`provider` VARCHAR(256) NOT NULL,
`extern_uid` VARCHAR(256) NOT NULL,
`created_ts` BIGINT NOT NULL DEFAULT (UNIX_TIMESTAMP()),
`updated_ts` BIGINT NOT NULL DEFAULT (UNIX_TIMESTAMP()),
UNIQUE (`provider`, `extern_uid`),
UNIQUE (`user_id`, `provider`)
);
CREATE INDEX `idx_user_identity_user_id` ON `user_identity`(`user_id`);
......@@ -109,3 +109,17 @@ CREATE TABLE `memo_share` (
);
CREATE INDEX `idx_memo_share_memo_id` ON `memo_share`(`memo_id`);
-- user_identity
CREATE TABLE `user_identity` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` INT NOT NULL,
`provider` VARCHAR(256) NOT NULL,
`extern_uid` VARCHAR(256) NOT NULL,
`created_ts` BIGINT NOT NULL DEFAULT (UNIX_TIMESTAMP()),
`updated_ts` BIGINT NOT NULL DEFAULT (UNIX_TIMESTAMP()),
UNIQUE (`provider`, `extern_uid`),
UNIQUE (`user_id`, `provider`)
);
CREATE INDEX `idx_user_identity_user_id` ON `user_identity`(`user_id`);
-- user_identity stores the linkage between an external identity subject and a local user.
-- (provider, extern_uid) is unique across the table; provider stores the idp.uid.
-- Each local user can link at most one external account per provider.
CREATE TABLE user_identity (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
provider TEXT NOT NULL,
extern_uid TEXT NOT NULL,
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
UNIQUE (provider, extern_uid),
UNIQUE (user_id, provider)
);
CREATE INDEX idx_user_identity_user_id ON user_identity(user_id);
......@@ -109,3 +109,17 @@ CREATE TABLE memo_share (
);
CREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id);
-- user_identity
CREATE TABLE user_identity (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
provider TEXT NOT NULL,
extern_uid TEXT NOT NULL,
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
UNIQUE (provider, extern_uid),
UNIQUE (user_id, provider)
);
CREATE INDEX idx_user_identity_user_id ON user_identity(user_id);
-- user_identity stores the linkage between an external identity subject and a local user.
-- (provider, extern_uid) is unique across the table; provider stores the idp.uid.
-- Each local user can link at most one external account per provider.
CREATE TABLE user_identity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
provider TEXT NOT NULL,
extern_uid TEXT NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE (provider, extern_uid),
UNIQUE (user_id, provider)
);
CREATE INDEX idx_user_identity_user_id ON user_identity(user_id);
......@@ -110,3 +110,17 @@ CREATE TABLE memo_share (
);
CREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id);
-- user_identity
CREATE TABLE user_identity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
provider TEXT NOT NULL,
extern_uid TEXT NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE (provider, extern_uid),
UNIQUE (user_id, provider)
);
CREATE INDEX idx_user_identity_user_id ON user_identity(user_id);
package test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/store"
)
func TestUserIdentityCreateAndGet(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
provider := "idp-uid-1"
externUID := "jane@example.com"
created, err := ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: provider,
ExternUID: externUID,
})
require.NoError(t, err)
require.NotZero(t, created.ID)
require.NotZero(t, created.CreatedTs)
require.Equal(t, user.ID, created.UserID)
require.Equal(t, provider, created.Provider)
require.Equal(t, externUID, created.ExternUID)
got, err := ts.GetUserIdentity(ctx, &store.FindUserIdentity{
Provider: &provider,
ExternUID: &externUID,
})
require.NoError(t, err)
require.NotNil(t, got)
require.Equal(t, created.ID, got.ID)
require.Equal(t, user.ID, got.UserID)
// Miss returns (nil, nil).
missingProvider := "idp-uid-missing"
notFound, err := ts.GetUserIdentity(ctx, &store.FindUserIdentity{
Provider: &missingProvider,
ExternUID: &externUID,
})
require.NoError(t, err)
require.Nil(t, notFound)
}
func TestUserIdentityListByUserID(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "idp-A",
ExternUID: "sub-a-1",
})
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "idp-B",
ExternUID: "sub-b-1",
})
require.NoError(t, err)
list, err := ts.ListUserIdentities(ctx, &store.FindUserIdentity{
UserID: &user.ID,
})
require.NoError(t, err)
require.Len(t, list, 2)
}
func TestUserIdentityUniqueConflict(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
userA, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
userB, err := createTestingUserWithRole(ctx, ts, "conflict_user", store.RoleUser)
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: userA.ID,
Provider: "idp-A",
ExternUID: "sub-1",
})
require.NoError(t, err)
// Second insert with the same (provider, extern_uid) must fail regardless of user_id.
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: userB.ID,
Provider: "idp-A",
ExternUID: "sub-1",
})
require.Error(t, err)
}
func TestUserIdentitySameExternUIDDifferentProviders(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "idp-A",
ExternUID: "sub-1",
})
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "idp-B",
ExternUID: "sub-1",
})
require.NoError(t, err)
externUID := "sub-1"
list, err := ts.ListUserIdentities(ctx, &store.FindUserIdentity{
ExternUID: &externUID,
})
require.NoError(t, err)
require.Len(t, list, 2)
}
func TestUserIdentitySameUserSameProviderConflicts(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "idp-A",
ExternUID: "sub-1",
})
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "idp-A",
ExternUID: "sub-2",
})
require.Error(t, err)
}
func TestUserIdentityDeleteByUserAndProvider(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "idp-A",
ExternUID: "sub-a-1",
})
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "idp-B",
ExternUID: "sub-b-1",
})
require.NoError(t, err)
provider := "idp-A"
err = ts.DeleteUserIdentities(ctx, &store.DeleteUserIdentity{
UserID: &user.ID,
Provider: &provider,
})
require.NoError(t, err)
list, err := ts.ListUserIdentities(ctx, &store.FindUserIdentity{
UserID: &user.ID,
})
require.NoError(t, err)
require.Len(t, list, 1)
require.Equal(t, "idp-B", list[0].Provider)
}
package store
import "context"
// UserIdentity is the linkage between an external identity subject and a local user.
// Uniqueness is enforced on (Provider, ExternUID); one local user may have multiple
// identities across different providers.
type UserIdentity struct {
ID int32
UserID int32
Provider string
ExternUID string
CreatedTs int64
UpdatedTs int64
}
// FindUserIdentity is used to filter user identities in list/get queries.
type FindUserIdentity struct {
ID *int32
UserID *int32
Provider *string
ExternUID *string
}
// DeleteUserIdentity is used to delete user identity linkage rows.
type DeleteUserIdentity struct {
ID *int32
UserID *int32
Provider *string
}
// CreateUserIdentity creates a new external-identity linkage record.
// Returns the driver error on unique-constraint violation; callers are responsible
// for reconciling concurrent first-login races on (Provider, ExternUID).
func (s *Store) CreateUserIdentity(ctx context.Context, create *UserIdentity) (*UserIdentity, error) {
return s.driver.CreateUserIdentity(ctx, create)
}
// ListUserIdentities returns all linkage records matching the filter.
func (s *Store) ListUserIdentities(ctx context.Context, find *FindUserIdentity) ([]*UserIdentity, error) {
return s.driver.ListUserIdentities(ctx, find)
}
// DeleteUserIdentities deletes all linkage records matching the filter.
func (s *Store) DeleteUserIdentities(ctx context.Context, delete *DeleteUserIdentity) error {
return s.driver.DeleteUserIdentities(ctx, delete)
}
// GetUserIdentity returns the first linkage record matching the filter, or nil if none found.
func (s *Store) GetUserIdentity(ctx context.Context, find *FindUserIdentity) (*UserIdentity, error) {
list, err := s.ListUserIdentities(ctx, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, nil
}
return list[0], nil
}
......@@ -21,6 +21,11 @@ type FindUserSetting struct {
Key storepb.UserSetting_Key
}
type DeleteUserSetting struct {
UserID *int32
Key storepb.UserSetting_Key
}
// RefreshTokenQueryResult contains the result of querying a refresh token.
type RefreshTokenQueryResult struct {
UserID int32
......@@ -102,6 +107,23 @@ func (s *Store) GetUserSetting(ctx context.Context, find *FindUserSetting) (*sto
return userSetting, nil
}
func (s *Store) DeleteUserSettings(ctx context.Context, delete *DeleteUserSetting) error {
existing, err := s.ListUserSettings(ctx, &FindUserSetting{
UserID: delete.UserID,
Key: delete.Key,
})
if err != nil {
return err
}
if err := s.driver.DeleteUserSettings(ctx, delete); err != nil {
return err
}
for _, setting := range existing {
s.userSettingCache.Delete(ctx, getUserSettingCacheKey(setting.UserId, setting.Key.String()))
}
return nil
}
// GetUserByPATHash finds a user by PAT hash.
func (s *Store) GetUserByPATHash(ctx context.Context, tokenHash string) (*PATQueryResult, error) {
result, err := s.driver.GetUserByPATHash(ctx, tokenHash)
......
import { useEffect, useMemo, useState } from "react";
import { toast } from "react-hot-toast";
import { Button } from "@/components/ui/button";
import { identityProviderServiceClient, userServiceClient } from "@/connect";
import { absolutifyLink } from "@/helpers/utils";
import useCurrentUser from "@/hooks/useCurrentUser";
import { handleError } from "@/lib/error";
import { IdentityProvider, IdentityProvider_Type } from "@/types/proto/api/v1/idp_service_pb";
import { LinkedIdentity } from "@/types/proto/api/v1/user_service_pb";
import { useTranslate } from "@/utils/i18n";
import { storeOAuthState } from "@/utils/oauth";
import SettingGroup from "./SettingGroup";
import SettingTable from "./SettingTable";
interface LinkedIdentityRow extends Record<string, unknown> {
name: string;
title: string;
externUid: string;
linkedIdentity?: LinkedIdentity;
identityProvider: IdentityProvider;
}
const LinkedIdentitySection = () => {
const t = useTranslate();
const currentUser = useCurrentUser();
const [identityProviderList, setIdentityProviderList] = useState<IdentityProvider[]>([]);
const [linkedIdentityList, setLinkedIdentityList] = useState<LinkedIdentity[]>([]);
const fetchData = async () => {
if (!currentUser?.name) {
return;
}
const [{ identityProviders }, { linkedIdentities }] = await Promise.all([
identityProviderServiceClient.listIdentityProviders({}),
userServiceClient.listLinkedIdentities({ parent: currentUser.name }),
]);
setIdentityProviderList(identityProviders);
setLinkedIdentityList(linkedIdentities);
};
useEffect(() => {
if (!currentUser?.name) {
return;
}
fetchData().catch((error: unknown) => {
handleError(error, toast.error, {
context: "Load linked identities",
});
});
}, [currentUser?.name]);
const oauthIdentityProviders = useMemo(
() => identityProviderList.filter((identityProvider) => identityProvider.type === IdentityProvider_Type.OAUTH2),
[identityProviderList],
);
const linkedIdentityByProviderName = useMemo(() => {
const mapping = new Map<string, LinkedIdentity>();
for (const linkedIdentity of linkedIdentityList) {
if (!mapping.has(linkedIdentity.idpName)) {
mapping.set(linkedIdentity.idpName, linkedIdentity);
}
}
return mapping;
}, [linkedIdentityList]);
const rows = useMemo<LinkedIdentityRow[]>(
() =>
oauthIdentityProviders.map((identityProvider) => {
const linkedIdentity = linkedIdentityByProviderName.get(identityProvider.name);
return {
name: identityProvider.name,
title: identityProvider.title,
externUid: linkedIdentity?.externUid ?? "",
linkedIdentity,
identityProvider,
};
}),
[linkedIdentityByProviderName, oauthIdentityProviders],
);
const handleLinkIdentityProvider = async (identityProvider: IdentityProvider) => {
if (!currentUser?.name) {
return;
}
const redirectUri = absolutifyLink("/auth/callback");
const oauth2Config = identityProvider.config?.config?.case === "oauth2Config" ? identityProvider.config.config.value : undefined;
if (!oauth2Config) {
toast.error("Identity provider configuration is invalid.");
return;
}
try {
const returnUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`;
const { state, codeChallenge } = await storeOAuthState(identityProvider.name, "link", returnUrl, currentUser.name);
let authUrl = `${oauth2Config.authUrl}?client_id=${
oauth2Config.clientId
}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${state}&response_type=code&scope=${encodeURIComponent(
oauth2Config.scopes.join(" "),
)}`;
if (codeChallenge) {
authUrl += `&code_challenge=${codeChallenge}&code_challenge_method=S256`;
}
window.location.href = authUrl;
} catch (error) {
handleError(error, toast.error, {
context: "Failed to initiate OAuth flow",
fallbackMessage: "Failed to initiate account linking. Please try again.",
});
}
};
const handleUnlinkIdentityProvider = async (row: LinkedIdentityRow) => {
if (!row.linkedIdentity?.name) {
return;
}
try {
await userServiceClient.deleteLinkedIdentity({
name: row.linkedIdentity.name,
});
await fetchData();
toast.success(`Unlinked ${row.title}.`);
} catch (error) {
handleError(error, toast.error, {
context: "Delete linked identity",
fallbackMessage: "Failed to unlink identity provider.",
});
}
};
if (oauthIdentityProviders.length === 0) {
return null;
}
return (
<SettingGroup
showSeparator
title="SSO accounts"
description="Each provider can be linked to this account at most once. A linked row shows the current extern_uid and can be unlinked."
>
<SettingTable<LinkedIdentityRow>
columns={[
{
key: "title",
header: "SSO provider",
render: (_, row: LinkedIdentityRow) => <span className="text-foreground">{row.title}</span>,
},
{
key: "externUid",
header: "extern_uid",
render: (_, row: LinkedIdentityRow) => (
<span className={row.externUid ? "text-foreground" : "text-muted-foreground"}>
{row.externUid || t("attachment-library.labels.not-linked")}
</span>
),
},
{
key: "actions",
header: "",
className: "text-right",
render: (_, row: LinkedIdentityRow) =>
row.linkedIdentity ? (
<Button variant="outline" size="sm" onClick={() => handleUnlinkIdentityProvider(row)}>
Unlink
</Button>
) : (
<Button variant="outline" size="sm" onClick={() => handleLinkIdentityProvider(row.identityProvider)}>
{t("common.link")}
</Button>
),
},
]}
data={rows}
emptyMessage="No SSO providers found."
getRowKey={(row) => row.name}
/>
</SettingGroup>
);
};
export default LinkedIdentitySection;
import { MoreVerticalIcon, PenLineIcon } from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";
import ConfirmDialog from "@/components/ConfirmDialog";
import { Button } from "@/components/ui/button";
import { userServiceClient } from "@/connect";
import { useAuth } from "@/contexts/AuthContext";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useDialog } from "@/hooks/useDialog";
import useNavigateTo from "@/hooks/useNavigateTo";
import { handleError } from "@/lib/error";
import { ROUTES } from "@/router/routes";
import { useTranslate } from "@/utils/i18n";
import ChangeMemberPasswordDialog from "../ChangeMemberPasswordDialog";
import UpdateAccountDialog from "../UpdateAccountDialog";
import UserAvatar from "../UserAvatar";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/dropdown-menu";
import AccessTokenSection from "./AccessTokenSection";
import LinkedIdentitySection from "./LinkedIdentitySection";
import SettingGroup from "./SettingGroup";
import SettingSection from "./SettingSection";
const MyAccountSection = () => {
const t = useTranslate();
const user = useCurrentUser();
const { logout } = useAuth();
const navigateTo = useNavigateTo();
const accountDialog = useDialog();
const passwordDialog = useDialog();
const [deleteOpen, setDeleteOpen] = useState(false);
const handleDeleteAccount = async () => {
if (!user?.name) {
return;
}
try {
await userServiceClient.deleteUser({ name: user.name });
await logout();
toast.success(t("setting.member.delete-success", { username: user.username }));
navigateTo(ROUTES.AUTH, { replace: true });
} catch (error) {
handleError(error, toast.error, { context: "Delete account" });
throw error;
}
};
return (
<SettingSection title={t("setting.my-account.label")}>
......@@ -42,21 +69,35 @@ const MyAccountSection = () => {
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={passwordDialog.open}>{t("setting.account.change-password")}</DropdownMenuItem>
<DropdownMenuItem onClick={() => setDeleteOpen(true)} className="text-destructive focus:text-destructive">
{t("setting.account.delete-account")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</SettingGroup>
<SettingGroup showSeparator>
<AccessTokenSection />
</SettingGroup>
<LinkedIdentitySection />
<AccessTokenSection />
{/* Update Account Dialog */}
<UpdateAccountDialog open={accountDialog.isOpen} onOpenChange={accountDialog.setOpen} />
{/* Change Password Dialog */}
<ChangeMemberPasswordDialog open={passwordDialog.isOpen} onOpenChange={passwordDialog.setOpen} user={user} />
<ConfirmDialog
open={deleteOpen}
onOpenChange={setDeleteOpen}
title={user ? t("setting.member.delete-warning", { username: user.username }) : ""}
description={t("setting.member.delete-warning-description")}
confirmLabel={t("common.delete")}
cancelLabel={t("common.cancel")}
onConfirm={handleDeleteAccount}
confirmVariant="destructive"
/>
</SettingSection>
);
};
......
import { MoreVerticalIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { toast } from "react-hot-toast";
import ConfirmDialog from "@/components/ConfirmDialog";
import { Button } from "@/components/ui/button";
......@@ -7,13 +7,23 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
import { identityProviderServiceClient } from "@/connect";
import { useDialog } from "@/hooks/useDialog";
import { handleError } from "@/lib/error";
import { IdentityProvider } from "@/types/proto/api/v1/idp_service_pb";
import { IdentityProvider, IdentityProvider_Type } from "@/types/proto/api/v1/idp_service_pb";
import { useTranslate } from "@/utils/i18n";
import CreateIdentityProviderDialog from "../CreateIdentityProviderDialog";
import LearnMore from "../LearnMore";
import SettingSection from "./SettingSection";
import SettingTable from "./SettingTable";
interface IdentityProviderRow extends Record<string, unknown> {
name: string;
providerUid: string;
title: string;
typeLabel: string;
provider: IdentityProvider;
}
const getIdentityProviderUID = (name: string) => name.replace(/^identity-providers\//, "");
const SSOSection = () => {
const t = useTranslate();
const [identityProviderList, setIdentityProviderList] = useState<IdentityProvider[]>([]);
......@@ -22,14 +32,32 @@ const SSOSection = () => {
const idpDialog = useDialog();
const fetchIdentityProviderList = async () => {
const { identityProviders } = await identityProviderServiceClient.listIdentityProviders({});
setIdentityProviderList(identityProviders);
try {
const { identityProviders } = await identityProviderServiceClient.listIdentityProviders({});
setIdentityProviderList(identityProviders);
} catch (error: unknown) {
handleError(error, toast.error, {
context: "Load identity providers",
});
}
};
useEffect(() => {
fetchIdentityProviderList();
void fetchIdentityProviderList();
}, []);
const rows = useMemo<IdentityProviderRow[]>(
() =>
identityProviderList.map((provider) => ({
name: provider.name,
providerUid: getIdentityProviderUID(provider.name),
title: provider.title,
typeLabel: IdentityProvider_Type[provider.type] ?? "TYPE_UNSPECIFIED",
provider,
})),
[identityProviderList],
);
const handleDeleteIdentityProvider = (identityProvider: IdentityProvider) => {
setDeleteTarget(identityProvider);
};
......@@ -88,20 +116,25 @@ const SSOSection = () => {
<SettingTable
columns={[
{
key: "title",
header: t("common.name"),
render: (_, provider: IdentityProvider) => (
<span className="text-foreground">
{provider.title}
<span className="ml-2 text-sm text-muted-foreground">({provider.type})</span>
</span>
key: "providerUid",
header: "provider_uid",
render: (_, row: IdentityProviderRow) => (
<div className="flex flex-col">
<span className="text-foreground">{row.providerUid}</span>
{row.title ? <span className="text-sm text-muted-foreground">{row.title}</span> : null}
</div>
),
},
{
key: "typeLabel",
header: t("common.type"),
render: (_, row: IdentityProviderRow) => <span className="text-muted-foreground">{row.typeLabel}</span>,
},
{
key: "actions",
header: "",
className: "text-right",
render: (_, provider: IdentityProvider) => (
render: (_, row: IdentityProviderRow) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
......@@ -109,9 +142,9 @@ const SSOSection = () => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={2}>
<DropdownMenuItem onClick={() => handleEditIdentityProvider(provider)}>{t("common.edit")}</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEditIdentityProvider(row.provider)}>{t("common.edit")}</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDeleteIdentityProvider(provider)}
onClick={() => handleDeleteIdentityProvider(row.provider)}
className="text-destructive focus:text-destructive"
>
{t("common.delete")}
......@@ -121,9 +154,9 @@ const SSOSection = () => {
),
},
]}
data={identityProviderList}
data={rows}
emptyMessage={t("setting.sso.no-sso-found")}
getRowKey={(provider) => provider.name}
getRowKey={(row) => row.name}
/>
<CreateIdentityProviderDialog
......
......@@ -386,6 +386,7 @@
},
"account": {
"change-password": "Change password",
"delete-account": "Delete account",
"email-note": "Optional",
"export-memos": "Export Memos",
"nickname-note": "Displayed in the banner",
......
......@@ -493,6 +493,7 @@
},
"account": {
"change-password": "修改密码",
"delete-account": "删除账号",
"email-note": "可选",
"export-memos": "导出备忘录",
"nickname-note": "显示在横幅中",
......
......@@ -2,7 +2,7 @@ import { timestampDate } from "@bufbuild/protobuf/wkt";
import { useEffect, useRef, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { setAccessToken } from "@/auth-state";
import { authServiceClient } from "@/connect";
import { authServiceClient, userServiceClient } from "@/connect";
import { useAuth } from "@/contexts/AuthContext";
import { absolutifyLink } from "@/helpers/utils";
import useNavigateTo from "@/hooks/useNavigateTo";
......@@ -18,7 +18,7 @@ interface State {
const AuthCallback = () => {
const navigateTo = useNavigateTo();
const { initialize } = useAuth();
const { currentUser, initialize, isInitialized } = useAuth();
const [searchParams] = useSearchParams();
const handledRef = useRef(false);
const [state, setState] = useState<State>({
......@@ -27,10 +27,12 @@ const AuthCallback = () => {
});
useEffect(() => {
if (!isInitialized) {
return;
}
if (handledRef.current) {
return;
}
handledRef.current = true;
// Check for OAuth error response first (e.g., user denied access)
const error = searchParams.get("error");
const errorDescription = searchParams.get("error_description");
......@@ -74,25 +76,42 @@ const AuthCallback = () => {
return;
}
const { identityProviderName, returnUrl, codeVerifier } = validatedState;
const { flowMode, identityProviderName, returnUrl, linkingUserName, codeVerifier } = validatedState;
const redirectUri = absolutifyLink("/auth/callback");
handledRef.current = true;
(async () => {
try {
const response = await authServiceClient.signIn({
credentials: {
case: "ssoCredentials",
value: {
idpName: identityProviderName,
code,
redirectUri,
codeVerifier: codeVerifier || "", // Pass PKCE code_verifier for token exchange
if (flowMode === "link") {
if (!currentUser?.name) {
throw new Error("Failed to link account. Please sign in to Memos again and retry.");
}
if (linkingUserName && currentUser.name !== linkingUserName) {
throw new Error("The signed-in user changed before the OAuth callback completed. Please retry linking from account settings.");
}
await userServiceClient.createLinkedIdentity({
parent: currentUser.name,
idpName: identityProviderName,
code,
redirectUri,
codeVerifier: codeVerifier || "",
});
} else {
const response = await authServiceClient.signIn({
credentials: {
case: "ssoCredentials",
value: {
idpName: identityProviderName,
code,
redirectUri,
codeVerifier: codeVerifier || "", // Pass PKCE code_verifier for token exchange
},
},
},
});
// Store access token from login response
if (response.accessToken) {
setAccessToken(response.accessToken, response.accessTokenExpiresAt ? timestampDate(response.accessTokenExpiresAt) : undefined);
});
// Store access token from login response
if (response.accessToken) {
setAccessToken(response.accessToken, response.accessTokenExpiresAt ? timestampDate(response.accessTokenExpiresAt) : undefined);
}
}
setState({
loading: false,
......@@ -116,7 +135,7 @@ const AuthCallback = () => {
});
}
})();
}, [searchParams, navigateTo]);
}, [currentUser?.name, initialize, isInitialized, navigateTo, searchParams]);
if (state.loading) return null;
......
......@@ -44,7 +44,7 @@ const SignIn = () => {
try {
// Generate and store secure state parameter with CSRF protection
// Also generate PKCE parameters (code_challenge) for enhanced security if available
const { state, codeChallenge } = await storeOAuthState(identityProvider.name, redirectTarget);
const { state, codeChallenge } = await storeOAuthState(identityProvider.name, "signin", redirectTarget);
// Build OAuth authorization URL with secure state
// Include PKCE if available (requires HTTPS/localhost for crypto.subtle)
......
......@@ -18,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file api/v1/user_service.proto.
*/
export const file_api_v1_user_service: GenFile = /*@__PURE__*/
fileDesc("ChlhcGkvdjEvdXNlcl9zZXJ2aWNlLnByb3RvEgxtZW1vcy5hcGkudjEi1gMKBFVzZXISEQoEbmFtZRgBIAEoCUID4EEIEioKBHJvbGUYAiABKA4yFy5tZW1vcy5hcGkudjEuVXNlci5Sb2xlQgPgQQISFQoIdXNlcm5hbWUYAyABKAlCA+BBAhISCgVlbWFpbBgEIAEoCUID4EEBEhkKDGRpc3BsYXlfbmFtZRgFIAEoCUID4EEBEhcKCmF2YXRhcl91cmwYBiABKAlCA+BBARIYCgtkZXNjcmlwdGlvbhgHIAEoCUID4EEBEhUKCHBhc3N3b3JkGAggASgJQgPgQQQSJwoFc3RhdGUYCSABKA4yEy5tZW1vcy5hcGkudjEuU3RhdGVCA+BBAhI0CgtjcmVhdGVfdGltZRgKIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxI0Cgt1cGRhdGVfdGltZRgLIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAyIxCgRSb2xlEhQKEFJPTEVfVU5TUEVDSUZJRUQQABIJCgVBRE1JThACEggKBFVTRVIQAzo36kE0ChFtZW1vcy5hcGkudjEvVXNlchIMdXNlcnMve3VzZXJ9GgRuYW1lKgV1c2VyczIEdXNlciJzChBMaXN0VXNlcnNSZXF1ZXN0EhYKCXBhZ2Vfc2l6ZRgBIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAiABKAlCA+BBARITCgZmaWx0ZXIYAyABKAlCA+BBARIZCgxzaG93X2RlbGV0ZWQYBCABKAhCA+BBASJjChFMaXN0VXNlcnNSZXNwb25zZRIhCgV1c2VycxgBIAMoCzISLm1lbW9zLmFwaS52MS5Vc2VyEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRISCgp0b3RhbF9zaXplGAMgASgFIikKFEJhdGNoR2V0VXNlcnNSZXF1ZXN0EhEKCXVzZXJuYW1lcxgBIAMoCSI6ChVCYXRjaEdldFVzZXJzUmVzcG9uc2USIQoFdXNlcnMYASADKAsyEi5tZW1vcy5hcGkudjEuVXNlciJtCg5HZXRVc2VyUmVxdWVzdBInCgRuYW1lGAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEjIKCXJlYWRfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCA+BBASKIAQoRQ3JlYXRlVXNlclJlcXVlc3QSKAoEdXNlchgBIAEoCzISLm1lbW9zLmFwaS52MS5Vc2VyQgbgQQLgQQQSFAoHdXNlcl9pZBgCIAEoCUID4EEBEhoKDXZhbGlkYXRlX29ubHkYAyABKAhCA+BBARIXCgpyZXF1ZXN0X2lkGAQgASgJQgPgQQEijAEKEVVwZGF0ZVVzZXJSZXF1ZXN0EiUKBHVzZXIYASABKAsyEi5tZW1vcy5hcGkudjEuVXNlckID4EECEjQKC3VwZGF0ZV9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0ID4EECEhoKDWFsbG93X21pc3NpbmcYAyABKAhCA+BBASJQChFEZWxldGVVc2VyUmVxdWVzdBInCgRuYW1lGAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhIKBWZvcmNlGAIgASgIQgPgQQEi2AMKCVVzZXJTdGF0cxIRCgRuYW1lGAEgASgJQgPgQQgSOwoXbWVtb19kaXNwbGF5X3RpbWVzdGFtcHMYAiADKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEj4KD21lbW9fdHlwZV9zdGF0cxgDIAEoCzIlLm1lbW9zLmFwaS52MS5Vc2VyU3RhdHMuTWVtb1R5cGVTdGF0cxI4Cgl0YWdfY291bnQYBCADKAsyJS5tZW1vcy5hcGkudjEuVXNlclN0YXRzLlRhZ0NvdW50RW50cnkSFAoMcGlubmVkX21lbW9zGAUgAygJEhgKEHRvdGFsX21lbW9fY291bnQYBiABKAUaLwoNVGFnQ291bnRFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAU6AjgBGl8KDU1lbW9UeXBlU3RhdHMSEgoKbGlua19jb3VudBgBIAEoBRISCgpjb2RlX2NvdW50GAIgASgFEhIKCnRvZG9fY291bnQYAyABKAUSEgoKdW5kb19jb3VudBgEIAEoBTo/6kE8ChZtZW1vcy5hcGkudjEvVXNlclN0YXRzEgx1c2Vycy97dXNlcn0qCXVzZXJTdGF0czIJdXNlclN0YXRzIj4KE0dldFVzZXJTdGF0c1JlcXVlc3QSJwoEbmFtZRgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlciIZChdMaXN0QWxsVXNlclN0YXRzUmVxdWVzdCJCChhMaXN0QWxsVXNlclN0YXRzUmVzcG9uc2USJgoFc3RhdHMYASADKAsyFy5tZW1vcy5hcGkudjEuVXNlclN0YXRzIuQDCgtVc2VyU2V0dGluZxIRCgRuYW1lGAEgASgJQgPgQQgSQwoPZ2VuZXJhbF9zZXR0aW5nGAIgASgLMigubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nLkdlbmVyYWxTZXR0aW5nSAASRQoQd2ViaG9va3Nfc2V0dGluZxgFIAEoCzIpLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZy5XZWJob29rc1NldHRpbmdIABpXCg5HZW5lcmFsU2V0dGluZxITCgZsb2NhbGUYASABKAlCA+BBARIcCg9tZW1vX3Zpc2liaWxpdHkYAyABKAlCA+BBARISCgV0aGVtZRgEIAEoCUID4EEBGj4KD1dlYmhvb2tzU2V0dGluZxIrCgh3ZWJob29rcxgBIAMoCzIZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9vayI1CgNLZXkSEwoPS0VZX1VOU1BFQ0lGSUVEEAASCwoHR0VORVJBTBABEgwKCFdFQkhPT0tTEAQ6XepBWgoYbWVtb3MuYXBpLnYxL1VzZXJTZXR0aW5nEiN1c2Vycy97dXNlcm5hbWV9L3NldHRpbmdzL3tzZXR0aW5nfSoMdXNlclNldHRpbmdzMgt1c2VyU2V0dGluZ0IHCgV2YWx1ZSJHChVHZXRVc2VyU2V0dGluZ1JlcXVlc3QSLgoEbmFtZRgBIAEoCUIg4EEC+kEaChhtZW1vcy5hcGkudjEvVXNlclNldHRpbmcigQEKGFVwZGF0ZVVzZXJTZXR0aW5nUmVxdWVzdBIvCgdzZXR0aW5nGAEgASgLMhkubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nQgPgQQISNAoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQIidQoXTGlzdFVzZXJTZXR0aW5nc1JlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhYKCXBhZ2Vfc2l6ZRgCIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAyABKAlCA+BBASJ0ChhMaXN0VXNlclNldHRpbmdzUmVzcG9uc2USKwoIc2V0dGluZ3MYASADKAsyGS5tZW1vcy5hcGkudjEuVXNlclNldHRpbmcSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhIKCnRvdGFsX3NpemUYAyABKAUi8gIKE1BlcnNvbmFsQWNjZXNzVG9rZW4SEQoEbmFtZRgBIAEoCUID4EEIEhgKC2Rlc2NyaXB0aW9uGAIgASgJQgPgQQESMwoKY3JlYXRlZF9hdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxIzCgpleHBpcmVzX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEBEjUKDGxhc3RfdXNlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAzqMAepBiAEKIG1lbW9zLmFwaS52MS9QZXJzb25hbEFjY2Vzc1Rva2VuEjl1c2Vycy97dXNlcn0vcGVyc29uYWxBY2Nlc3NUb2tlbnMve3BlcnNvbmFsX2FjY2Vzc190b2tlbn0qFHBlcnNvbmFsQWNjZXNzVG9rZW5zMhNwZXJzb25hbEFjY2Vzc1Rva2VuIn0KH0xpc3RQZXJzb25hbEFjY2Vzc1Rva2Vuc1JlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhYKCXBhZ2Vfc2l6ZRgCIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAyABKAlCA+BBASKSAQogTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zUmVzcG9uc2USQQoWcGVyc29uYWxfYWNjZXNzX3Rva2VucxgBIAMoCzIhLm1lbW9zLmFwaS52MS5QZXJzb25hbEFjY2Vzc1Rva2VuEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRISCgp0b3RhbF9zaXplGAMgASgFIoUBCiBDcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVxdWVzdBIpCgZwYXJlbnQYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISGAoLZGVzY3JpcHRpb24YAiABKAlCA+BBARIcCg9leHBpcmVzX2luX2RheXMYAyABKAVCA+BBASJ0CiFDcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVzcG9uc2USQAoVcGVyc29uYWxfYWNjZXNzX3Rva2VuGAEgASgLMiEubWVtb3MuYXBpLnYxLlBlcnNvbmFsQWNjZXNzVG9rZW4SDQoFdG9rZW4YAiABKAkiWgogRGVsZXRlUGVyc29uYWxBY2Nlc3NUb2tlblJlcXVlc3QSNgoEbmFtZRgBIAEoCUIo4EEC+kEiCiBtZW1vcy5hcGkudjEvUGVyc29uYWxBY2Nlc3NUb2tlbiKqAQoLVXNlcldlYmhvb2sSDAoEbmFtZRgBIAEoCRILCgN1cmwYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEjQKC2NyZWF0ZV90aW1lGAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDEjQKC3VwZGF0ZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDIi4KF0xpc3RVc2VyV2ViaG9va3NSZXF1ZXN0EhMKBnBhcmVudBgBIAEoCUID4EECIkcKGExpc3RVc2VyV2ViaG9va3NSZXNwb25zZRIrCgh3ZWJob29rcxgBIAMoCzIZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9vayJgChhDcmVhdGVVc2VyV2ViaG9va1JlcXVlc3QSEwoGcGFyZW50GAEgASgJQgPgQQISLwoHd2ViaG9vaxgCIAEoCzIZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9va0ID4EECInwKGFVwZGF0ZVVzZXJXZWJob29rUmVxdWVzdBIvCgd3ZWJob29rGAEgASgLMhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rQgPgQQISLwoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrIi0KGERlbGV0ZVVzZXJXZWJob29rUmVxdWVzdBIRCgRuYW1lGAEgASgJQgPgQQIiogcKEFVzZXJOb3RpZmljYXRpb24SFAoEbmFtZRgBIAEoCUIG4EED4EEIEikKBnNlbmRlchgCIAEoCUIZ4EED+kETChFtZW1vcy5hcGkudjEvVXNlchIsCgtzZW5kZXJfdXNlchgIIAEoCzISLm1lbW9zLmFwaS52MS5Vc2VyQgPgQQMSOgoGc3RhdHVzGAMgASgOMiUubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb24uU3RhdHVzQgPgQQESNAoLY3JlYXRlX3RpbWUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQMSNgoEdHlwZRgFIAEoDjIjLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uLlR5cGVCA+BBAxJOCgxtZW1vX2NvbW1lbnQYBiABKAsyMS5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbi5NZW1vQ29tbWVudFBheWxvYWRCA+BBA0gAEk4KDG1lbW9fbWVudGlvbhgHIAEoCzIxLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uLk1lbW9NZW50aW9uUGF5bG9hZEID4EEDSAAabAoSTWVtb0NvbW1lbnRQYXlsb2FkEgwKBG1lbW8YASABKAkSFAoMcmVsYXRlZF9tZW1vGAIgASgJEhQKDG1lbW9fc25pcHBldBgDIAEoCRIcChRyZWxhdGVkX21lbW9fc25pcHBldBgEIAEoCRpsChJNZW1vTWVudGlvblBheWxvYWQSDAoEbWVtbxgBIAEoCRIUCgxyZWxhdGVkX21lbW8YAiABKAkSFAoMbWVtb19zbmlwcGV0GAMgASgJEhwKFHJlbGF0ZWRfbWVtb19zbmlwcGV0GAQgASgJIjoKBlN0YXR1cxIWChJTVEFUVVNfVU5TUEVDSUZJRUQQABIKCgZVTlJFQUQQARIMCghBUkNISVZFRBACIkAKBFR5cGUSFAoQVFlQRV9VTlNQRUNJRklFRBAAEhAKDE1FTU9fQ09NTUVOVBABEhAKDE1FTU9fTUVOVElPThACOnDqQW0KHW1lbW9zLmFwaS52MS9Vc2VyTm90aWZpY2F0aW9uEil1c2Vycy97dXNlcn0vbm90aWZpY2F0aW9ucy97bm90aWZpY2F0aW9ufRoEbmFtZSoNbm90aWZpY2F0aW9uczIMbm90aWZpY2F0aW9uQgkKB3BheWxvYWQijwEKHExpc3RVc2VyTm90aWZpY2F0aW9uc1JlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhYKCXBhZ2Vfc2l6ZRgCIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAyABKAlCA+BBARITCgZmaWx0ZXIYBCABKAlCA+BBASJvCh1MaXN0VXNlck5vdGlmaWNhdGlvbnNSZXNwb25zZRI1Cg1ub3RpZmljYXRpb25zGAEgAygLMh4ubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIpABCh1VcGRhdGVVc2VyTm90aWZpY2F0aW9uUmVxdWVzdBI5Cgxub3RpZmljYXRpb24YASABKAsyHi5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbkID4EECEjQKC3VwZGF0ZV9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0ID4EECIlQKHURlbGV0ZVVzZXJOb3RpZmljYXRpb25SZXF1ZXN0EjMKBG5hbWUYASABKAlCJeBBAvpBHwodbWVtb3MuYXBpLnYxL1VzZXJOb3RpZmljYXRpb24ygBgKC1VzZXJTZXJ2aWNlEmMKCUxpc3RVc2VycxIeLm1lbW9zLmFwaS52MS5MaXN0VXNlcnNSZXF1ZXN0Gh8ubWVtb3MuYXBpLnYxLkxpc3RVc2Vyc1Jlc3BvbnNlIhWC0+STAg8SDS9hcGkvdjEvdXNlcnMSewoNQmF0Y2hHZXRVc2VycxIiLm1lbW9zLmFwaS52MS5CYXRjaEdldFVzZXJzUmVxdWVzdBojLm1lbW9zLmFwaS52MS5CYXRjaEdldFVzZXJzUmVzcG9uc2UiIYLT5JMCGzoBKiIWL2FwaS92MS91c2VyczpiYXRjaEdldBJiCgdHZXRVc2VyEhwubWVtb3MuYXBpLnYxLkdldFVzZXJSZXF1ZXN0GhIubWVtb3MuYXBpLnYxLlVzZXIiJdpBBG5hbWWC0+STAhgSFi9hcGkvdjEve25hbWU9dXNlcnMvKn0SZQoKQ3JlYXRlVXNlchIfLm1lbW9zLmFwaS52MS5DcmVhdGVVc2VyUmVxdWVzdBoSLm1lbW9zLmFwaS52MS5Vc2VyIiLaQQR1c2VygtPkkwIVOgR1c2VyIg0vYXBpL3YxL3VzZXJzEn8KClVwZGF0ZVVzZXISHy5tZW1vcy5hcGkudjEuVXBkYXRlVXNlclJlcXVlc3QaEi5tZW1vcy5hcGkudjEuVXNlciI82kEQdXNlcix1cGRhdGVfbWFza4LT5JMCIzoEdXNlcjIbL2FwaS92MS97dXNlci5uYW1lPXVzZXJzLyp9EmwKCkRlbGV0ZVVzZXISHy5tZW1vcy5hcGkudjEuRGVsZXRlVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiJdpBBG5hbWWC0+STAhgqFi9hcGkvdjEve25hbWU9dXNlcnMvKn0SfgoQTGlzdEFsbFVzZXJTdGF0cxIlLm1lbW9zLmFwaS52MS5MaXN0QWxsVXNlclN0YXRzUmVxdWVzdBomLm1lbW9zLmFwaS52MS5MaXN0QWxsVXNlclN0YXRzUmVzcG9uc2UiG4LT5JMCFRITL2FwaS92MS91c2VyczpzdGF0cxJ6CgxHZXRVc2VyU3RhdHMSIS5tZW1vcy5hcGkudjEuR2V0VXNlclN0YXRzUmVxdWVzdBoXLm1lbW9zLmFwaS52MS5Vc2VyU3RhdHMiLtpBBG5hbWWC0+STAiESHy9hcGkvdjEve25hbWU9dXNlcnMvKn06Z2V0U3RhdHMSggEKDkdldFVzZXJTZXR0aW5nEiMubWVtb3MuYXBpLnYxLkdldFVzZXJTZXR0aW5nUmVxdWVzdBoZLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZyIw2kEEbmFtZYLT5JMCIxIhL2FwaS92MS97bmFtZT11c2Vycy8qL3NldHRpbmdzLyp9EqgBChFVcGRhdGVVc2VyU2V0dGluZxImLm1lbW9zLmFwaS52MS5VcGRhdGVVc2VyU2V0dGluZ1JlcXVlc3QaGS5tZW1vcy5hcGkudjEuVXNlclNldHRpbmciUNpBE3NldHRpbmcsdXBkYXRlX21hc2uC0+STAjQ6B3NldHRpbmcyKS9hcGkvdjEve3NldHRpbmcubmFtZT11c2Vycy8qL3NldHRpbmdzLyp9EpUBChBMaXN0VXNlclNldHRpbmdzEiUubWVtb3MuYXBpLnYxLkxpc3RVc2VyU2V0dGluZ3NSZXF1ZXN0GiYubWVtb3MuYXBpLnYxLkxpc3RVc2VyU2V0dGluZ3NSZXNwb25zZSIy2kEGcGFyZW50gtPkkwIjEiEvYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vc2V0dGluZ3MSuQEKGExpc3RQZXJzb25hbEFjY2Vzc1Rva2VucxItLm1lbW9zLmFwaS52MS5MaXN0UGVyc29uYWxBY2Nlc3NUb2tlbnNSZXF1ZXN0Gi4ubWVtb3MuYXBpLnYxLkxpc3RQZXJzb25hbEFjY2Vzc1Rva2Vuc1Jlc3BvbnNlIj7aQQZwYXJlbnSC0+STAi8SLS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS9wZXJzb25hbEFjY2Vzc1Rva2VucxK2AQoZQ3JlYXRlUGVyc29uYWxBY2Nlc3NUb2tlbhIuLm1lbW9zLmFwaS52MS5DcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVxdWVzdBovLm1lbW9zLmFwaS52MS5DcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVzcG9uc2UiOILT5JMCMjoBKiItL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3BlcnNvbmFsQWNjZXNzVG9rZW5zEqEBChlEZWxldGVQZXJzb25hbEFjY2Vzc1Rva2VuEi4ubWVtb3MuYXBpLnYxLkRlbGV0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IjzaQQRuYW1lgtPkkwIvKi0vYXBpL3YxL3tuYW1lPXVzZXJzLyovcGVyc29uYWxBY2Nlc3NUb2tlbnMvKn0SlQEKEExpc3RVc2VyV2ViaG9va3MSJS5tZW1vcy5hcGkudjEuTGlzdFVzZXJXZWJob29rc1JlcXVlc3QaJi5tZW1vcy5hcGkudjEuTGlzdFVzZXJXZWJob29rc1Jlc3BvbnNlIjLaQQZwYXJlbnSC0+STAiMSIS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS93ZWJob29rcxKbAQoRQ3JlYXRlVXNlcldlYmhvb2sSJi5tZW1vcy5hcGkudjEuQ3JlYXRlVXNlcldlYmhvb2tSZXF1ZXN0GhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rIkPaQQ5wYXJlbnQsd2ViaG9va4LT5JMCLDoHd2ViaG9vayIhL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3dlYmhvb2tzEqgBChFVcGRhdGVVc2VyV2ViaG9vaxImLm1lbW9zLmFwaS52MS5VcGRhdGVVc2VyV2ViaG9va1JlcXVlc3QaGS5tZW1vcy5hcGkudjEuVXNlcldlYmhvb2siUNpBE3dlYmhvb2ssdXBkYXRlX21hc2uC0+STAjQ6B3dlYmhvb2syKS9hcGkvdjEve3dlYmhvb2submFtZT11c2Vycy8qL3dlYmhvb2tzLyp9EoUBChFEZWxldGVVc2VyV2ViaG9vaxImLm1lbW9zLmFwaS52MS5EZWxldGVVc2VyV2ViaG9va1JlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiMNpBBG5hbWWC0+STAiMqIS9hcGkvdjEve25hbWU9dXNlcnMvKi93ZWJob29rcy8qfRKpAQoVTGlzdFVzZXJOb3RpZmljYXRpb25zEioubWVtb3MuYXBpLnYxLkxpc3RVc2VyTm90aWZpY2F0aW9uc1JlcXVlc3QaKy5tZW1vcy5hcGkudjEuTGlzdFVzZXJOb3RpZmljYXRpb25zUmVzcG9uc2UiN9pBBnBhcmVudILT5JMCKBImL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L25vdGlmaWNhdGlvbnMSywEKFlVwZGF0ZVVzZXJOb3RpZmljYXRpb24SKy5tZW1vcy5hcGkudjEuVXBkYXRlVXNlck5vdGlmaWNhdGlvblJlcXVlc3QaHi5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbiJk2kEYbm90aWZpY2F0aW9uLHVwZGF0ZV9tYXNrgtPkkwJDOgxub3RpZmljYXRpb24yMy9hcGkvdjEve25vdGlmaWNhdGlvbi5uYW1lPXVzZXJzLyovbm90aWZpY2F0aW9ucy8qfRKUAQoWRGVsZXRlVXNlck5vdGlmaWNhdGlvbhIrLm1lbW9zLmFwaS52MS5EZWxldGVVc2VyTm90aWZpY2F0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSI12kEEbmFtZYLT5JMCKComL2FwaS92MS97bmFtZT11c2Vycy8qL25vdGlmaWNhdGlvbnMvKn1CqAEKEGNvbS5tZW1vcy5hcGkudjFCEFVzZXJTZXJ2aWNlUHJvdG9QAVowZ2l0aHViLmNvbS91c2VtZW1vcy9tZW1vcy9wcm90by9nZW4vYXBpL3YxO2FwaXYxogIDTUFYqgIMTWVtb3MuQXBpLlYxygIMTWVtb3NcQXBpXFYx4gIYTWVtb3NcQXBpXFYxXEdQQk1ldGFkYXRh6gIOTWVtb3M6OkFwaTo6VjFiBnByb3RvMw", [file_api_v1_common, file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_empty, file_google_protobuf_field_mask, file_google_protobuf_timestamp]);
fileDesc("ChlhcGkvdjEvdXNlcl9zZXJ2aWNlLnByb3RvEgxtZW1vcy5hcGkudjEi1gMKBFVzZXISEQoEbmFtZRgBIAEoCUID4EEIEioKBHJvbGUYAiABKA4yFy5tZW1vcy5hcGkudjEuVXNlci5Sb2xlQgPgQQISFQoIdXNlcm5hbWUYAyABKAlCA+BBAhISCgVlbWFpbBgEIAEoCUID4EEBEhkKDGRpc3BsYXlfbmFtZRgFIAEoCUID4EEBEhcKCmF2YXRhcl91cmwYBiABKAlCA+BBARIYCgtkZXNjcmlwdGlvbhgHIAEoCUID4EEBEhUKCHBhc3N3b3JkGAggASgJQgPgQQQSJwoFc3RhdGUYCSABKA4yEy5tZW1vcy5hcGkudjEuU3RhdGVCA+BBAhI0CgtjcmVhdGVfdGltZRgKIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxI0Cgt1cGRhdGVfdGltZRgLIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAyIxCgRSb2xlEhQKEFJPTEVfVU5TUEVDSUZJRUQQABIJCgVBRE1JThACEggKBFVTRVIQAzo36kE0ChFtZW1vcy5hcGkudjEvVXNlchIMdXNlcnMve3VzZXJ9GgRuYW1lKgV1c2VyczIEdXNlciJzChBMaXN0VXNlcnNSZXF1ZXN0EhYKCXBhZ2Vfc2l6ZRgBIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAiABKAlCA+BBARITCgZmaWx0ZXIYAyABKAlCA+BBARIZCgxzaG93X2RlbGV0ZWQYBCABKAhCA+BBASJjChFMaXN0VXNlcnNSZXNwb25zZRIhCgV1c2VycxgBIAMoCzISLm1lbW9zLmFwaS52MS5Vc2VyEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRISCgp0b3RhbF9zaXplGAMgASgFIikKFEJhdGNoR2V0VXNlcnNSZXF1ZXN0EhEKCXVzZXJuYW1lcxgBIAMoCSI6ChVCYXRjaEdldFVzZXJzUmVzcG9uc2USIQoFdXNlcnMYASADKAsyEi5tZW1vcy5hcGkudjEuVXNlciJtCg5HZXRVc2VyUmVxdWVzdBInCgRuYW1lGAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEjIKCXJlYWRfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCA+BBASKIAQoRQ3JlYXRlVXNlclJlcXVlc3QSKAoEdXNlchgBIAEoCzISLm1lbW9zLmFwaS52MS5Vc2VyQgbgQQLgQQQSFAoHdXNlcl9pZBgCIAEoCUID4EEBEhoKDXZhbGlkYXRlX29ubHkYAyABKAhCA+BBARIXCgpyZXF1ZXN0X2lkGAQgASgJQgPgQQEijAEKEVVwZGF0ZVVzZXJSZXF1ZXN0EiUKBHVzZXIYASABKAsyEi5tZW1vcy5hcGkudjEuVXNlckID4EECEjQKC3VwZGF0ZV9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0ID4EECEhoKDWFsbG93X21pc3NpbmcYAyABKAhCA+BBASJQChFEZWxldGVVc2VyUmVxdWVzdBInCgRuYW1lGAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhIKBWZvcmNlGAIgASgIQgPgQQEi2AMKCVVzZXJTdGF0cxIRCgRuYW1lGAEgASgJQgPgQQgSOwoXbWVtb19kaXNwbGF5X3RpbWVzdGFtcHMYAiADKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEj4KD21lbW9fdHlwZV9zdGF0cxgDIAEoCzIlLm1lbW9zLmFwaS52MS5Vc2VyU3RhdHMuTWVtb1R5cGVTdGF0cxI4Cgl0YWdfY291bnQYBCADKAsyJS5tZW1vcy5hcGkudjEuVXNlclN0YXRzLlRhZ0NvdW50RW50cnkSFAoMcGlubmVkX21lbW9zGAUgAygJEhgKEHRvdGFsX21lbW9fY291bnQYBiABKAUaXwoNTWVtb1R5cGVTdGF0cxISCgpsaW5rX2NvdW50GAEgASgFEhIKCmNvZGVfY291bnQYAiABKAUSEgoKdG9kb19jb3VudBgDIAEoBRISCgp1bmRvX2NvdW50GAQgASgFGi8KDVRhZ0NvdW50RW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgFOgI4ATo/6kE8ChZtZW1vcy5hcGkudjEvVXNlclN0YXRzEgx1c2Vycy97dXNlcn0qCXVzZXJTdGF0czIJdXNlclN0YXRzIj4KE0dldFVzZXJTdGF0c1JlcXVlc3QSJwoEbmFtZRgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlciIZChdMaXN0QWxsVXNlclN0YXRzUmVxdWVzdCJCChhMaXN0QWxsVXNlclN0YXRzUmVzcG9uc2USJgoFc3RhdHMYASADKAsyFy5tZW1vcy5hcGkudjEuVXNlclN0YXRzIuQDCgtVc2VyU2V0dGluZxIRCgRuYW1lGAEgASgJQgPgQQgSQwoPZ2VuZXJhbF9zZXR0aW5nGAIgASgLMigubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nLkdlbmVyYWxTZXR0aW5nSAASRQoQd2ViaG9va3Nfc2V0dGluZxgFIAEoCzIpLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZy5XZWJob29rc1NldHRpbmdIABpXCg5HZW5lcmFsU2V0dGluZxITCgZsb2NhbGUYASABKAlCA+BBARIcCg9tZW1vX3Zpc2liaWxpdHkYAyABKAlCA+BBARISCgV0aGVtZRgEIAEoCUID4EEBGj4KD1dlYmhvb2tzU2V0dGluZxIrCgh3ZWJob29rcxgBIAMoCzIZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9vayI1CgNLZXkSEwoPS0VZX1VOU1BFQ0lGSUVEEAASCwoHR0VORVJBTBABEgwKCFdFQkhPT0tTEAQ6XepBWgoYbWVtb3MuYXBpLnYxL1VzZXJTZXR0aW5nEiN1c2Vycy97dXNlcm5hbWV9L3NldHRpbmdzL3tzZXR0aW5nfSoMdXNlclNldHRpbmdzMgt1c2VyU2V0dGluZ0IHCgV2YWx1ZSJHChVHZXRVc2VyU2V0dGluZ1JlcXVlc3QSLgoEbmFtZRgBIAEoCUIg4EEC+kEaChhtZW1vcy5hcGkudjEvVXNlclNldHRpbmcigQEKGFVwZGF0ZVVzZXJTZXR0aW5nUmVxdWVzdBIvCgdzZXR0aW5nGAEgASgLMhkubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nQgPgQQISNAoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQIidQoXTGlzdFVzZXJTZXR0aW5nc1JlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhYKCXBhZ2Vfc2l6ZRgCIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAyABKAlCA+BBASJ0ChhMaXN0VXNlclNldHRpbmdzUmVzcG9uc2USKwoIc2V0dGluZ3MYASADKAsyGS5tZW1vcy5hcGkudjEuVXNlclNldHRpbmcSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhIKCnRvdGFsX3NpemUYAyABKAUi6gEKDkxpbmtlZElkZW50aXR5EhEKBG5hbWUYASABKAlCA+BBCBI3CghpZHBfbmFtZRgCIAEoCUIl4EED+kEfCh1tZW1vcy5hcGkudjEvSWRlbnRpdHlQcm92aWRlchIXCgpleHRlcm5fdWlkGAMgASgJQgPgQQM6c+pBcAobbWVtb3MuYXBpLnYxL0xpbmtlZElkZW50aXR5Ei91c2Vycy97dXNlcn0vbGlua2VkSWRlbnRpdGllcy97bGlua2VkX2lkZW50aXR5fSoQbGlua2VkSWRlbnRpdGllczIObGlua2VkSWRlbnRpdHkiSAobTGlzdExpbmtlZElkZW50aXRpZXNSZXF1ZXN0EikKBnBhcmVudBgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlciJXChxMaXN0TGlua2VkSWRlbnRpdGllc1Jlc3BvbnNlEjcKEWxpbmtlZF9pZGVudGl0aWVzGAEgAygLMhwubWVtb3MuYXBpLnYxLkxpbmtlZElkZW50aXR5IssBChtDcmVhdGVMaW5rZWRJZGVudGl0eVJlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEjcKCGlkcF9uYW1lGAIgASgJQiXgQQL6QR8KHW1lbW9zLmFwaS52MS9JZGVudGl0eVByb3ZpZGVyEhEKBGNvZGUYAyABKAlCA+BBAhIZCgxyZWRpcmVjdF91cmkYBCABKAlCA+BBAhIaCg1jb2RlX3ZlcmlmaWVyGAUgASgJQgPgQQEiTQoYR2V0TGlua2VkSWRlbnRpdHlSZXF1ZXN0EjEKBG5hbWUYASABKAlCI+BBAvpBHQobbWVtb3MuYXBpLnYxL0xpbmtlZElkZW50aXR5IlAKG0RlbGV0ZUxpbmtlZElkZW50aXR5UmVxdWVzdBIxCgRuYW1lGAEgASgJQiPgQQL6QR0KG21lbW9zLmFwaS52MS9MaW5rZWRJZGVudGl0eSLyAgoTUGVyc29uYWxBY2Nlc3NUb2tlbhIRCgRuYW1lGAEgASgJQgPgQQgSGAoLZGVzY3JpcHRpb24YAiABKAlCA+BBARIzCgpjcmVhdGVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDEjMKCmV4cGlyZXNfYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQESNQoMbGFzdF91c2VkX2F0GAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDOowB6kGIAQogbWVtb3MuYXBpLnYxL1BlcnNvbmFsQWNjZXNzVG9rZW4SOXVzZXJzL3t1c2VyfS9wZXJzb25hbEFjY2Vzc1Rva2Vucy97cGVyc29uYWxfYWNjZXNzX3Rva2VufSoUcGVyc29uYWxBY2Nlc3NUb2tlbnMyE3BlcnNvbmFsQWNjZXNzVG9rZW4ifQofTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zUmVxdWVzdBIpCgZwYXJlbnQYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISFgoJcGFnZV9zaXplGAIgASgFQgPgQQESFwoKcGFnZV90b2tlbhgDIAEoCUID4EEBIpIBCiBMaXN0UGVyc29uYWxBY2Nlc3NUb2tlbnNSZXNwb25zZRJBChZwZXJzb25hbF9hY2Nlc3NfdG9rZW5zGAEgAygLMiEubWVtb3MuYXBpLnYxLlBlcnNvbmFsQWNjZXNzVG9rZW4SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhIKCnRvdGFsX3NpemUYAyABKAUihQEKIENyZWF0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXF1ZXN0EikKBnBhcmVudBgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlchIYCgtkZXNjcmlwdGlvbhgCIAEoCUID4EEBEhwKD2V4cGlyZXNfaW5fZGF5cxgDIAEoBUID4EEBInQKIUNyZWF0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXNwb25zZRJAChVwZXJzb25hbF9hY2Nlc3NfdG9rZW4YASABKAsyIS5tZW1vcy5hcGkudjEuUGVyc29uYWxBY2Nlc3NUb2tlbhINCgV0b2tlbhgCIAEoCSJaCiBEZWxldGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVxdWVzdBI2CgRuYW1lGAEgASgJQijgQQL6QSIKIG1lbW9zLmFwaS52MS9QZXJzb25hbEFjY2Vzc1Rva2VuIqoBCgtVc2VyV2ViaG9vaxIMCgRuYW1lGAEgASgJEgsKA3VybBgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSNAoLY3JlYXRlX3RpbWUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQMSNAoLdXBkYXRlX3RpbWUYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQMiLgoXTGlzdFVzZXJXZWJob29rc1JlcXVlc3QSEwoGcGFyZW50GAEgASgJQgPgQQIiRwoYTGlzdFVzZXJXZWJob29rc1Jlc3BvbnNlEisKCHdlYmhvb2tzGAEgAygLMhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rImAKGENyZWF0ZVVzZXJXZWJob29rUmVxdWVzdBITCgZwYXJlbnQYASABKAlCA+BBAhIvCgd3ZWJob29rGAIgASgLMhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rQgPgQQIifAoYVXBkYXRlVXNlcldlYmhvb2tSZXF1ZXN0Ei8KB3dlYmhvb2sYASABKAsyGS5tZW1vcy5hcGkudjEuVXNlcldlYmhvb2tCA+BBAhIvCgt1cGRhdGVfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2siLQoYRGVsZXRlVXNlcldlYmhvb2tSZXF1ZXN0EhEKBG5hbWUYASABKAlCA+BBAiKiBwoQVXNlck5vdGlmaWNhdGlvbhIUCgRuYW1lGAEgASgJQgbgQQPgQQgSKQoGc2VuZGVyGAIgASgJQhngQQP6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEiwKC3NlbmRlcl91c2VyGAggASgLMhIubWVtb3MuYXBpLnYxLlVzZXJCA+BBAxI6CgZzdGF0dXMYAyABKA4yJS5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbi5TdGF0dXNCA+BBARI0CgtjcmVhdGVfdGltZRgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxI2CgR0eXBlGAUgASgOMiMubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb24uVHlwZUID4EEDEk4KDG1lbW9fY29tbWVudBgGIAEoCzIxLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uLk1lbW9Db21tZW50UGF5bG9hZEID4EEDSAASTgoMbWVtb19tZW50aW9uGAcgASgLMjEubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb24uTWVtb01lbnRpb25QYXlsb2FkQgPgQQNIABpsChJNZW1vQ29tbWVudFBheWxvYWQSDAoEbWVtbxgBIAEoCRIUCgxyZWxhdGVkX21lbW8YAiABKAkSFAoMbWVtb19zbmlwcGV0GAMgASgJEhwKFHJlbGF0ZWRfbWVtb19zbmlwcGV0GAQgASgJGmwKEk1lbW9NZW50aW9uUGF5bG9hZBIMCgRtZW1vGAEgASgJEhQKDHJlbGF0ZWRfbWVtbxgCIAEoCRIUCgxtZW1vX3NuaXBwZXQYAyABKAkSHAoUcmVsYXRlZF9tZW1vX3NuaXBwZXQYBCABKAkiOgoGU3RhdHVzEhYKElNUQVRVU19VTlNQRUNJRklFRBAAEgoKBlVOUkVBRBABEgwKCEFSQ0hJVkVEEAIiQAoEVHlwZRIUChBUWVBFX1VOU1BFQ0lGSUVEEAASEAoMTUVNT19DT01NRU5UEAESEAoMTUVNT19NRU5USU9OEAI6cOpBbQodbWVtb3MuYXBpLnYxL1VzZXJOb3RpZmljYXRpb24SKXVzZXJzL3t1c2VyfS9ub3RpZmljYXRpb25zL3tub3RpZmljYXRpb259GgRuYW1lKg1ub3RpZmljYXRpb25zMgxub3RpZmljYXRpb25CCQoHcGF5bG9hZCKPAQocTGlzdFVzZXJOb3RpZmljYXRpb25zUmVxdWVzdBIpCgZwYXJlbnQYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISFgoJcGFnZV9zaXplGAIgASgFQgPgQQESFwoKcGFnZV90b2tlbhgDIAEoCUID4EEBEhMKBmZpbHRlchgEIAEoCUID4EEBIm8KHUxpc3RVc2VyTm90aWZpY2F0aW9uc1Jlc3BvbnNlEjUKDW5vdGlmaWNhdGlvbnMYASADKAsyHi5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkikAEKHVVwZGF0ZVVzZXJOb3RpZmljYXRpb25SZXF1ZXN0EjkKDG5vdGlmaWNhdGlvbhgBIAEoCzIeLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uQgPgQQISNAoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQIiVAodRGVsZXRlVXNlck5vdGlmaWNhdGlvblJlcXVlc3QSMwoEbmFtZRgBIAEoCUIl4EEC+kEfCh1tZW1vcy5hcGkudjEvVXNlck5vdGlmaWNhdGlvbjKCHQoLVXNlclNlcnZpY2USYwoJTGlzdFVzZXJzEh4ubWVtb3MuYXBpLnYxLkxpc3RVc2Vyc1JlcXVlc3QaHy5tZW1vcy5hcGkudjEuTGlzdFVzZXJzUmVzcG9uc2UiFYLT5JMCDxINL2FwaS92MS91c2VycxJ7Cg1CYXRjaEdldFVzZXJzEiIubWVtb3MuYXBpLnYxLkJhdGNoR2V0VXNlcnNSZXF1ZXN0GiMubWVtb3MuYXBpLnYxLkJhdGNoR2V0VXNlcnNSZXNwb25zZSIhgtPkkwIbOgEqIhYvYXBpL3YxL3VzZXJzOmJhdGNoR2V0EmIKB0dldFVzZXISHC5tZW1vcy5hcGkudjEuR2V0VXNlclJlcXVlc3QaEi5tZW1vcy5hcGkudjEuVXNlciIl2kEEbmFtZYLT5JMCGBIWL2FwaS92MS97bmFtZT11c2Vycy8qfRJlCgpDcmVhdGVVc2VyEh8ubWVtb3MuYXBpLnYxLkNyZWF0ZVVzZXJSZXF1ZXN0GhIubWVtb3MuYXBpLnYxLlVzZXIiItpBBHVzZXKC0+STAhU6BHVzZXIiDS9hcGkvdjEvdXNlcnMSfwoKVXBkYXRlVXNlchIfLm1lbW9zLmFwaS52MS5VcGRhdGVVc2VyUmVxdWVzdBoSLm1lbW9zLmFwaS52MS5Vc2VyIjzaQRB1c2VyLHVwZGF0ZV9tYXNrgtPkkwIjOgR1c2VyMhsvYXBpL3YxL3t1c2VyLm5hbWU9dXNlcnMvKn0SbAoKRGVsZXRlVXNlchIfLm1lbW9zLmFwaS52MS5EZWxldGVVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIl2kEEbmFtZYLT5JMCGCoWL2FwaS92MS97bmFtZT11c2Vycy8qfRJ+ChBMaXN0QWxsVXNlclN0YXRzEiUubWVtb3MuYXBpLnYxLkxpc3RBbGxVc2VyU3RhdHNSZXF1ZXN0GiYubWVtb3MuYXBpLnYxLkxpc3RBbGxVc2VyU3RhdHNSZXNwb25zZSIbgtPkkwIVEhMvYXBpL3YxL3VzZXJzOnN0YXRzEnoKDEdldFVzZXJTdGF0cxIhLm1lbW9zLmFwaS52MS5HZXRVc2VyU3RhdHNSZXF1ZXN0GhcubWVtb3MuYXBpLnYxLlVzZXJTdGF0cyIu2kEEbmFtZYLT5JMCIRIfL2FwaS92MS97bmFtZT11c2Vycy8qfTpnZXRTdGF0cxKCAQoOR2V0VXNlclNldHRpbmcSIy5tZW1vcy5hcGkudjEuR2V0VXNlclNldHRpbmdSZXF1ZXN0GhkubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nIjDaQQRuYW1lgtPkkwIjEiEvYXBpL3YxL3tuYW1lPXVzZXJzLyovc2V0dGluZ3MvKn0SqAEKEVVwZGF0ZVVzZXJTZXR0aW5nEiYubWVtb3MuYXBpLnYxLlVwZGF0ZVVzZXJTZXR0aW5nUmVxdWVzdBoZLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZyJQ2kETc2V0dGluZyx1cGRhdGVfbWFza4LT5JMCNDoHc2V0dGluZzIpL2FwaS92MS97c2V0dGluZy5uYW1lPXVzZXJzLyovc2V0dGluZ3MvKn0SlQEKEExpc3RVc2VyU2V0dGluZ3MSJS5tZW1vcy5hcGkudjEuTGlzdFVzZXJTZXR0aW5nc1JlcXVlc3QaJi5tZW1vcy5hcGkudjEuTGlzdFVzZXJTZXR0aW5nc1Jlc3BvbnNlIjLaQQZwYXJlbnSC0+STAiMSIS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS9zZXR0aW5ncxKpAQoUTGlzdExpbmtlZElkZW50aXRpZXMSKS5tZW1vcy5hcGkudjEuTGlzdExpbmtlZElkZW50aXRpZXNSZXF1ZXN0GioubWVtb3MuYXBpLnYxLkxpc3RMaW5rZWRJZGVudGl0aWVzUmVzcG9uc2UiOtpBBnBhcmVudILT5JMCKxIpL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L2xpbmtlZElkZW50aXRpZXMSpwEKFENyZWF0ZUxpbmtlZElkZW50aXR5EikubWVtb3MuYXBpLnYxLkNyZWF0ZUxpbmtlZElkZW50aXR5UmVxdWVzdBocLm1lbW9zLmFwaS52MS5MaW5rZWRJZGVudGl0eSJG2kEPcGFyZW50LGlkcF9uYW1lgtPkkwIuOgEqIikvYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vbGlua2VkSWRlbnRpdGllcxKTAQoRR2V0TGlua2VkSWRlbnRpdHkSJi5tZW1vcy5hcGkudjEuR2V0TGlua2VkSWRlbnRpdHlSZXF1ZXN0GhwubWVtb3MuYXBpLnYxLkxpbmtlZElkZW50aXR5IjjaQQRuYW1lgtPkkwIrEikvYXBpL3YxL3tuYW1lPXVzZXJzLyovbGlua2VkSWRlbnRpdGllcy8qfRKTAQoURGVsZXRlTGlua2VkSWRlbnRpdHkSKS5tZW1vcy5hcGkudjEuRGVsZXRlTGlua2VkSWRlbnRpdHlSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IjjaQQRuYW1lgtPkkwIrKikvYXBpL3YxL3tuYW1lPXVzZXJzLyovbGlua2VkSWRlbnRpdGllcy8qfRK5AQoYTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zEi0ubWVtb3MuYXBpLnYxLkxpc3RQZXJzb25hbEFjY2Vzc1Rva2Vuc1JlcXVlc3QaLi5tZW1vcy5hcGkudjEuTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zUmVzcG9uc2UiPtpBBnBhcmVudILT5JMCLxItL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3BlcnNvbmFsQWNjZXNzVG9rZW5zErYBChlDcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuEi4ubWVtb3MuYXBpLnYxLkNyZWF0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXF1ZXN0Gi8ubWVtb3MuYXBpLnYxLkNyZWF0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXNwb25zZSI4gtPkkwIyOgEqIi0vYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vcGVyc29uYWxBY2Nlc3NUb2tlbnMSoQEKGURlbGV0ZVBlcnNvbmFsQWNjZXNzVG9rZW4SLi5tZW1vcy5hcGkudjEuRGVsZXRlUGVyc29uYWxBY2Nlc3NUb2tlblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiPNpBBG5hbWWC0+STAi8qLS9hcGkvdjEve25hbWU9dXNlcnMvKi9wZXJzb25hbEFjY2Vzc1Rva2Vucy8qfRKVAQoQTGlzdFVzZXJXZWJob29rcxIlLm1lbW9zLmFwaS52MS5MaXN0VXNlcldlYmhvb2tzUmVxdWVzdBomLm1lbW9zLmFwaS52MS5MaXN0VXNlcldlYmhvb2tzUmVzcG9uc2UiMtpBBnBhcmVudILT5JMCIxIhL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3dlYmhvb2tzEpsBChFDcmVhdGVVc2VyV2ViaG9vaxImLm1lbW9zLmFwaS52MS5DcmVhdGVVc2VyV2ViaG9va1JlcXVlc3QaGS5tZW1vcy5hcGkudjEuVXNlcldlYmhvb2siQ9pBDnBhcmVudCx3ZWJob29rgtPkkwIsOgd3ZWJob29rIiEvYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vd2ViaG9va3MSqAEKEVVwZGF0ZVVzZXJXZWJob29rEiYubWVtb3MuYXBpLnYxLlVwZGF0ZVVzZXJXZWJob29rUmVxdWVzdBoZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9vayJQ2kETd2ViaG9vayx1cGRhdGVfbWFza4LT5JMCNDoHd2ViaG9vazIpL2FwaS92MS97d2ViaG9vay5uYW1lPXVzZXJzLyovd2ViaG9va3MvKn0ShQEKEURlbGV0ZVVzZXJXZWJob29rEiYubWVtb3MuYXBpLnYxLkRlbGV0ZVVzZXJXZWJob29rUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIw2kEEbmFtZYLT5JMCIyohL2FwaS92MS97bmFtZT11c2Vycy8qL3dlYmhvb2tzLyp9EqkBChVMaXN0VXNlck5vdGlmaWNhdGlvbnMSKi5tZW1vcy5hcGkudjEuTGlzdFVzZXJOb3RpZmljYXRpb25zUmVxdWVzdBorLm1lbW9zLmFwaS52MS5MaXN0VXNlck5vdGlmaWNhdGlvbnNSZXNwb25zZSI32kEGcGFyZW50gtPkkwIoEiYvYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vbm90aWZpY2F0aW9ucxLLAQoWVXBkYXRlVXNlck5vdGlmaWNhdGlvbhIrLm1lbW9zLmFwaS52MS5VcGRhdGVVc2VyTm90aWZpY2F0aW9uUmVxdWVzdBoeLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uImTaQRhub3RpZmljYXRpb24sdXBkYXRlX21hc2uC0+STAkM6DG5vdGlmaWNhdGlvbjIzL2FwaS92MS97bm90aWZpY2F0aW9uLm5hbWU9dXNlcnMvKi9ub3RpZmljYXRpb25zLyp9EpQBChZEZWxldGVVc2VyTm90aWZpY2F0aW9uEisubWVtb3MuYXBpLnYxLkRlbGV0ZVVzZXJOb3RpZmljYXRpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IjXaQQRuYW1lgtPkkwIoKiYvYXBpL3YxL3tuYW1lPXVzZXJzLyovbm90aWZpY2F0aW9ucy8qfUKoAQoQY29tLm1lbW9zLmFwaS52MUIQVXNlclNlcnZpY2VQcm90b1ABWjBnaXRodWIuY29tL3VzZW1lbW9zL21lbW9zL3Byb3RvL2dlbi9hcGkvdjE7YXBpdjGiAgNNQViqAgxNZW1vcy5BcGkuVjHKAgxNZW1vc1xBcGlcVjHiAhhNZW1vc1xBcGlcVjFcR1BCTWV0YWRhdGHqAg5NZW1vczo6QXBpOjpWMWIGcHJvdG8z", [file_api_v1_common, file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_empty, file_google_protobuf_field_mask, file_google_protobuf_timestamp]);
/**
* @generated from message memos.api.v1.User
......@@ -785,6 +785,171 @@ export type ListUserSettingsResponse = Message<"memos.api.v1.ListUserSettingsRes
export const ListUserSettingsResponseSchema: GenMessage<ListUserSettingsResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 17);
/**
* LinkedIdentity represents an SSO identity linked to a user account.
*
* @generated from message memos.api.v1.LinkedIdentity
*/
export type LinkedIdentity = Message<"memos.api.v1.LinkedIdentity"> & {
/**
* The resource name of the linked identity.
* Format: users/{user}/linkedIdentities/{linked_identity}
*
* @generated from field: string name = 1;
*/
name: string;
/**
* The resource name of the identity provider.
* Format: identity-providers/{uid}
*
* @generated from field: string idp_name = 2;
*/
idpName: string;
/**
* The external user identifier from the identity provider.
*
* @generated from field: string extern_uid = 3;
*/
externUid: string;
};
/**
* Describes the message memos.api.v1.LinkedIdentity.
* Use `create(LinkedIdentitySchema)` to create a new message.
*/
export const LinkedIdentitySchema: GenMessage<LinkedIdentity> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 18);
/**
* @generated from message memos.api.v1.ListLinkedIdentitiesRequest
*/
export type ListLinkedIdentitiesRequest = Message<"memos.api.v1.ListLinkedIdentitiesRequest"> & {
/**
* Required. The parent resource whose linked identities will be listed.
* Format: users/{user}
*
* @generated from field: string parent = 1;
*/
parent: string;
};
/**
* Describes the message memos.api.v1.ListLinkedIdentitiesRequest.
* Use `create(ListLinkedIdentitiesRequestSchema)` to create a new message.
*/
export const ListLinkedIdentitiesRequestSchema: GenMessage<ListLinkedIdentitiesRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 19);
/**
* @generated from message memos.api.v1.ListLinkedIdentitiesResponse
*/
export type ListLinkedIdentitiesResponse = Message<"memos.api.v1.ListLinkedIdentitiesResponse"> & {
/**
* The list of linked identities.
*
* @generated from field: repeated memos.api.v1.LinkedIdentity linked_identities = 1;
*/
linkedIdentities: LinkedIdentity[];
};
/**
* Describes the message memos.api.v1.ListLinkedIdentitiesResponse.
* Use `create(ListLinkedIdentitiesResponseSchema)` to create a new message.
*/
export const ListLinkedIdentitiesResponseSchema: GenMessage<ListLinkedIdentitiesResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 20);
/**
* @generated from message memos.api.v1.CreateLinkedIdentityRequest
*/
export type CreateLinkedIdentityRequest = Message<"memos.api.v1.CreateLinkedIdentityRequest"> & {
/**
* Required. The parent user who owns the linked identity.
* Format: users/{user}
*
* @generated from field: string parent = 1;
*/
parent: string;
/**
* Required. The identity provider to link.
* Format: identity-providers/{uid}
*
* @generated from field: string idp_name = 2;
*/
idpName: string;
/**
* Required. The authorization code from the identity provider.
*
* @generated from field: string code = 3;
*/
code: string;
/**
* Required. The redirect URI used in the OAuth flow.
*
* @generated from field: string redirect_uri = 4;
*/
redirectUri: string;
/**
* Optional. The PKCE code verifier used in the OAuth flow.
*
* @generated from field: string code_verifier = 5;
*/
codeVerifier: string;
};
/**
* Describes the message memos.api.v1.CreateLinkedIdentityRequest.
* Use `create(CreateLinkedIdentityRequestSchema)` to create a new message.
*/
export const CreateLinkedIdentityRequestSchema: GenMessage<CreateLinkedIdentityRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 21);
/**
* @generated from message memos.api.v1.GetLinkedIdentityRequest
*/
export type GetLinkedIdentityRequest = Message<"memos.api.v1.GetLinkedIdentityRequest"> & {
/**
* Required. The resource name of the linked identity to get.
* Format: users/{user}/linkedIdentities/{linked_identity}
*
* @generated from field: string name = 1;
*/
name: string;
};
/**
* Describes the message memos.api.v1.GetLinkedIdentityRequest.
* Use `create(GetLinkedIdentityRequestSchema)` to create a new message.
*/
export const GetLinkedIdentityRequestSchema: GenMessage<GetLinkedIdentityRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 22);
/**
* @generated from message memos.api.v1.DeleteLinkedIdentityRequest
*/
export type DeleteLinkedIdentityRequest = Message<"memos.api.v1.DeleteLinkedIdentityRequest"> & {
/**
* Required. The resource name of the linked identity to delete.
* Format: users/{user}/linkedIdentities/{linked_identity}
*
* @generated from field: string name = 1;
*/
name: string;
};
/**
* Describes the message memos.api.v1.DeleteLinkedIdentityRequest.
* Use `create(DeleteLinkedIdentityRequestSchema)` to create a new message.
*/
export const DeleteLinkedIdentityRequestSchema: GenMessage<DeleteLinkedIdentityRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 23);
/**
* PersonalAccessToken represents a long-lived token for API/script access.
* PATs are distinct from short-lived JWT access tokens used for session authentication.
......@@ -834,7 +999,7 @@ export type PersonalAccessToken = Message<"memos.api.v1.PersonalAccessToken"> &
* Use `create(PersonalAccessTokenSchema)` to create a new message.
*/
export const PersonalAccessTokenSchema: GenMessage<PersonalAccessToken> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 18);
messageDesc(file_api_v1_user_service, 24);
/**
* @generated from message memos.api.v1.ListPersonalAccessTokensRequest
......@@ -868,7 +1033,7 @@ export type ListPersonalAccessTokensRequest = Message<"memos.api.v1.ListPersonal
* Use `create(ListPersonalAccessTokensRequestSchema)` to create a new message.
*/
export const ListPersonalAccessTokensRequestSchema: GenMessage<ListPersonalAccessTokensRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 19);
messageDesc(file_api_v1_user_service, 25);
/**
* @generated from message memos.api.v1.ListPersonalAccessTokensResponse
......@@ -901,7 +1066,7 @@ export type ListPersonalAccessTokensResponse = Message<"memos.api.v1.ListPersona
* Use `create(ListPersonalAccessTokensResponseSchema)` to create a new message.
*/
export const ListPersonalAccessTokensResponseSchema: GenMessage<ListPersonalAccessTokensResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 20);
messageDesc(file_api_v1_user_service, 26);
/**
* @generated from message memos.api.v1.CreatePersonalAccessTokenRequest
......@@ -935,7 +1100,7 @@ export type CreatePersonalAccessTokenRequest = Message<"memos.api.v1.CreatePerso
* Use `create(CreatePersonalAccessTokenRequestSchema)` to create a new message.
*/
export const CreatePersonalAccessTokenRequestSchema: GenMessage<CreatePersonalAccessTokenRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 21);
messageDesc(file_api_v1_user_service, 27);
/**
* @generated from message memos.api.v1.CreatePersonalAccessTokenResponse
......@@ -962,7 +1127,7 @@ export type CreatePersonalAccessTokenResponse = Message<"memos.api.v1.CreatePers
* Use `create(CreatePersonalAccessTokenResponseSchema)` to create a new message.
*/
export const CreatePersonalAccessTokenResponseSchema: GenMessage<CreatePersonalAccessTokenResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 22);
messageDesc(file_api_v1_user_service, 28);
/**
* @generated from message memos.api.v1.DeletePersonalAccessTokenRequest
......@@ -982,7 +1147,7 @@ export type DeletePersonalAccessTokenRequest = Message<"memos.api.v1.DeletePerso
* Use `create(DeletePersonalAccessTokenRequestSchema)` to create a new message.
*/
export const DeletePersonalAccessTokenRequestSchema: GenMessage<DeletePersonalAccessTokenRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 23);
messageDesc(file_api_v1_user_service, 29);
/**
* UserWebhook represents a webhook owned by a user.
......@@ -1032,7 +1197,7 @@ export type UserWebhook = Message<"memos.api.v1.UserWebhook"> & {
* Use `create(UserWebhookSchema)` to create a new message.
*/
export const UserWebhookSchema: GenMessage<UserWebhook> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 24);
messageDesc(file_api_v1_user_service, 30);
/**
* @generated from message memos.api.v1.ListUserWebhooksRequest
......@@ -1052,7 +1217,7 @@ export type ListUserWebhooksRequest = Message<"memos.api.v1.ListUserWebhooksRequ
* Use `create(ListUserWebhooksRequestSchema)` to create a new message.
*/
export const ListUserWebhooksRequestSchema: GenMessage<ListUserWebhooksRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 25);
messageDesc(file_api_v1_user_service, 31);
/**
* @generated from message memos.api.v1.ListUserWebhooksResponse
......@@ -1071,7 +1236,7 @@ export type ListUserWebhooksResponse = Message<"memos.api.v1.ListUserWebhooksRes
* Use `create(ListUserWebhooksResponseSchema)` to create a new message.
*/
export const ListUserWebhooksResponseSchema: GenMessage<ListUserWebhooksResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 26);
messageDesc(file_api_v1_user_service, 32);
/**
* @generated from message memos.api.v1.CreateUserWebhookRequest
......@@ -1098,7 +1263,7 @@ export type CreateUserWebhookRequest = Message<"memos.api.v1.CreateUserWebhookRe
* Use `create(CreateUserWebhookRequestSchema)` to create a new message.
*/
export const CreateUserWebhookRequestSchema: GenMessage<CreateUserWebhookRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 27);
messageDesc(file_api_v1_user_service, 33);
/**
* @generated from message memos.api.v1.UpdateUserWebhookRequest
......@@ -1124,7 +1289,7 @@ export type UpdateUserWebhookRequest = Message<"memos.api.v1.UpdateUserWebhookRe
* Use `create(UpdateUserWebhookRequestSchema)` to create a new message.
*/
export const UpdateUserWebhookRequestSchema: GenMessage<UpdateUserWebhookRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 28);
messageDesc(file_api_v1_user_service, 34);
/**
* @generated from message memos.api.v1.DeleteUserWebhookRequest
......@@ -1144,7 +1309,7 @@ export type DeleteUserWebhookRequest = Message<"memos.api.v1.DeleteUserWebhookRe
* Use `create(DeleteUserWebhookRequestSchema)` to create a new message.
*/
export const DeleteUserWebhookRequestSchema: GenMessage<DeleteUserWebhookRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 29);
messageDesc(file_api_v1_user_service, 35);
/**
* @generated from message memos.api.v1.UserNotification
......@@ -1217,7 +1382,7 @@ export type UserNotification = Message<"memos.api.v1.UserNotification"> & {
* Use `create(UserNotificationSchema)` to create a new message.
*/
export const UserNotificationSchema: GenMessage<UserNotification> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 30);
messageDesc(file_api_v1_user_service, 36);
/**
* @generated from message memos.api.v1.UserNotification.MemoCommentPayload
......@@ -1259,7 +1424,7 @@ export type UserNotification_MemoCommentPayload = Message<"memos.api.v1.UserNoti
* Use `create(UserNotification_MemoCommentPayloadSchema)` to create a new message.
*/
export const UserNotification_MemoCommentPayloadSchema: GenMessage<UserNotification_MemoCommentPayload> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 30, 0);
messageDesc(file_api_v1_user_service, 36, 0);
/**
* @generated from message memos.api.v1.UserNotification.MemoMentionPayload
......@@ -1301,7 +1466,7 @@ export type UserNotification_MemoMentionPayload = Message<"memos.api.v1.UserNoti
* Use `create(UserNotification_MemoMentionPayloadSchema)` to create a new message.
*/
export const UserNotification_MemoMentionPayloadSchema: GenMessage<UserNotification_MemoMentionPayload> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 30, 1);
messageDesc(file_api_v1_user_service, 36, 1);
/**
* @generated from enum memos.api.v1.UserNotification.Status
......@@ -1327,7 +1492,7 @@ export enum UserNotification_Status {
* Describes the enum memos.api.v1.UserNotification.Status.
*/
export const UserNotification_StatusSchema: GenEnum<UserNotification_Status> = /*@__PURE__*/
enumDesc(file_api_v1_user_service, 30, 0);
enumDesc(file_api_v1_user_service, 36, 0);
/**
* @generated from enum memos.api.v1.UserNotification.Type
......@@ -1353,7 +1518,7 @@ export enum UserNotification_Type {
* Describes the enum memos.api.v1.UserNotification.Type.
*/
export const UserNotification_TypeSchema: GenEnum<UserNotification_Type> = /*@__PURE__*/
enumDesc(file_api_v1_user_service, 30, 1);
enumDesc(file_api_v1_user_service, 36, 1);
/**
* @generated from message memos.api.v1.ListUserNotificationsRequest
......@@ -1388,7 +1553,7 @@ export type ListUserNotificationsRequest = Message<"memos.api.v1.ListUserNotific
* Use `create(ListUserNotificationsRequestSchema)` to create a new message.
*/
export const ListUserNotificationsRequestSchema: GenMessage<ListUserNotificationsRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 31);
messageDesc(file_api_v1_user_service, 37);
/**
* @generated from message memos.api.v1.ListUserNotificationsResponse
......@@ -1410,7 +1575,7 @@ export type ListUserNotificationsResponse = Message<"memos.api.v1.ListUserNotifi
* Use `create(ListUserNotificationsResponseSchema)` to create a new message.
*/
export const ListUserNotificationsResponseSchema: GenMessage<ListUserNotificationsResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 32);
messageDesc(file_api_v1_user_service, 38);
/**
* @generated from message memos.api.v1.UpdateUserNotificationRequest
......@@ -1432,7 +1597,7 @@ export type UpdateUserNotificationRequest = Message<"memos.api.v1.UpdateUserNoti
* Use `create(UpdateUserNotificationRequestSchema)` to create a new message.
*/
export const UpdateUserNotificationRequestSchema: GenMessage<UpdateUserNotificationRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 33);
messageDesc(file_api_v1_user_service, 39);
/**
* @generated from message memos.api.v1.DeleteUserNotificationRequest
......@@ -1451,7 +1616,7 @@ export type DeleteUserNotificationRequest = Message<"memos.api.v1.DeleteUserNoti
* Use `create(DeleteUserNotificationRequestSchema)` to create a new message.
*/
export const DeleteUserNotificationRequestSchema: GenMessage<DeleteUserNotificationRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 34);
messageDesc(file_api_v1_user_service, 40);
/**
* @generated from service memos.api.v1.UserService
......@@ -1568,6 +1733,46 @@ export const UserService: GenService<{
input: typeof ListUserSettingsRequestSchema;
output: typeof ListUserSettingsResponseSchema;
},
/**
* ListLinkedIdentities returns a list of linked SSO identities for a user.
*
* @generated from rpc memos.api.v1.UserService.ListLinkedIdentities
*/
listLinkedIdentities: {
methodKind: "unary";
input: typeof ListLinkedIdentitiesRequestSchema;
output: typeof ListLinkedIdentitiesResponseSchema;
},
/**
* CreateLinkedIdentity links an SSO identity to the authenticated user.
*
* @generated from rpc memos.api.v1.UserService.CreateLinkedIdentity
*/
createLinkedIdentity: {
methodKind: "unary";
input: typeof CreateLinkedIdentityRequestSchema;
output: typeof LinkedIdentitySchema;
},
/**
* GetLinkedIdentity gets a linked SSO identity for a user.
*
* @generated from rpc memos.api.v1.UserService.GetLinkedIdentity
*/
getLinkedIdentity: {
methodKind: "unary";
input: typeof GetLinkedIdentityRequestSchema;
output: typeof LinkedIdentitySchema;
},
/**
* DeleteLinkedIdentity unlinks an SSO identity from a user.
*
* @generated from rpc memos.api.v1.UserService.DeleteLinkedIdentity
*/
deleteLinkedIdentity: {
methodKind: "unary";
input: typeof DeleteLinkedIdentityRequestSchema;
output: typeof EmptySchema;
},
/**
* ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
* PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
......
const STATE_STORAGE_KEY = "oauth_state";
const STATE_EXPIRY_MS = 10 * 60 * 1000; // 10 minutes
export type OAuthFlowMode = "signin" | "link";
interface OAuthState {
state: string;
identityProviderName: string;
flowMode: OAuthFlowMode;
timestamp: number;
returnUrl?: string;
linkingUserName?: string;
codeVerifier?: string; // PKCE code_verifier
}
......@@ -44,7 +48,9 @@ function base64UrlEncode(buffer: Uint8Array): string {
// PKCE is optional - if crypto APIs are unavailable (HTTP context), falls back to standard OAuth
export async function storeOAuthState(
identityProviderName: string,
flowMode: OAuthFlowMode,
returnUrl?: string,
linkingUserName?: string,
): Promise<{ state: string; codeChallenge?: string }> {
const state = generateSecureState();
......@@ -74,8 +80,10 @@ export async function storeOAuthState(
const stateData: OAuthState = {
state,
identityProviderName,
flowMode,
timestamp: Date.now(),
returnUrl,
linkingUserName,
codeVerifier, // Store for later retrieval in callback (undefined if PKCE not available)
};
......@@ -90,8 +98,10 @@ export async function storeOAuthState(
}
// Validate and retrieve OAuth state from storage (CSRF protection)
// Returns identityProviderName, returnUrl, and codeVerifier for PKCE
export function validateOAuthState(stateParam: string): { identityProviderName: string; returnUrl?: string; codeVerifier?: string } | null {
// Returns identityProviderName, flowMode, returnUrl, linkingUserName, and codeVerifier for PKCE
export function validateOAuthState(
stateParam: string,
): { identityProviderName: string; flowMode: OAuthFlowMode; returnUrl?: string; linkingUserName?: string; codeVerifier?: string } | null {
try {
const storedData = sessionStorage.getItem(STATE_STORAGE_KEY);
if (!storedData) {
......@@ -119,7 +129,9 @@ export function validateOAuthState(stateParam: string): { identityProviderName:
sessionStorage.removeItem(STATE_STORAGE_KEY);
return {
identityProviderName: stateData.identityProviderName,
flowMode: stateData.flowMode || "signin",
returnUrl: stateData.returnUrl,
linkingUserName: stateData.linkingUserName,
codeVerifier: stateData.codeVerifier, // Return PKCE code_verifier
};
} catch (error) {
......
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { storeOAuthState, validateOAuthState } from "@/utils/oauth";
describe("oauth state", () => {
beforeEach(() => {
sessionStorage.clear();
});
afterEach(() => {
sessionStorage.clear();
});
it("round-trips the linking user for link flows", async () => {
const { state } = await storeOAuthState("identity-providers/google", "link", "/settings", "users/alice");
expect(validateOAuthState(state)).toEqual({
identityProviderName: "identity-providers/google",
flowMode: "link",
returnUrl: "/settings",
linkingUserName: "users/alice",
codeVerifier: expect.any(String),
});
});
it("defaults older states to signin without a linking user", () => {
sessionStorage.setItem(
"oauth_state",
JSON.stringify({
state: "legacy-state",
identityProviderName: "identity-providers/google",
timestamp: Date.now(),
returnUrl: "/auth",
}),
);
expect(validateOAuthState("legacy-state")).toEqual({
identityProviderName: "identity-providers/google",
flowMode: "signin",
returnUrl: "/auth",
linkingUserName: undefined,
codeVerifier: undefined,
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment