mirror of
https://github.com/mirego/accent.git
synced 2024-09-11 10:55:28 +03:00
Add api token create/revoke
This commit is contained in:
parent
84741452e9
commit
bb8979d325
2
cli/package-lock.json
generated
2
cli/package-lock.json
generated
@ -6,7 +6,7 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "accent-cli",
|
||||
"version": "0.10.2",
|
||||
"version": "0.12.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oclif/command": "1.8.16",
|
||||
|
@ -9,7 +9,7 @@ import ConfigFetcher from './services/config';
|
||||
import ProjectFetcher from './services/project-fetcher';
|
||||
|
||||
// Types
|
||||
import {Project} from './types/project';
|
||||
import {Project, ProjectViewer} from './types/project';
|
||||
|
||||
const sleep = async (ms: number) =>
|
||||
new Promise((resolve: (value: unknown) => void) => setTimeout(resolve, ms));
|
||||
@ -17,6 +17,7 @@ const sleep = async (ms: number) =>
|
||||
export default abstract class extends Command {
|
||||
projectConfig: ConfigFetcher = new ConfigFetcher();
|
||||
project?: Project;
|
||||
viewer?: ProjectViewer;
|
||||
|
||||
async init() {
|
||||
const config = this.projectConfig.config;
|
||||
@ -26,10 +27,13 @@ export default abstract class extends Command {
|
||||
await sleep(1000);
|
||||
|
||||
const fetcher = new ProjectFetcher();
|
||||
this.project = await fetcher.fetch(config);
|
||||
const response = await fetcher.fetch(config);
|
||||
this.project = response.project;
|
||||
this.viewer = response;
|
||||
|
||||
if (!this.project) error('Unable to fetch project');
|
||||
|
||||
cli.action.stop(chalk.green('✓'));
|
||||
cli.action.stop(chalk.green(`${this.viewer.user.fullname} ✓`));
|
||||
|
||||
if (this.projectConfig.warnings.length) {
|
||||
console.log('');
|
||||
|
@ -43,7 +43,8 @@ export default class ProjectStatsFormatter extends Base {
|
||||
(memo, revision: Revision) => memo + revision.reviewedCount,
|
||||
0
|
||||
);
|
||||
const percentageReviewed = reviewedCount / translationsCount;
|
||||
const percentageReviewed =
|
||||
translationsCount > 0 ? reviewedCount / translationsCount : 0;
|
||||
|
||||
const percentageReviewedString = `${percentageReviewed}% reviewed`;
|
||||
let percentageReviewedFormat = chalk.green(percentageReviewedString);
|
||||
@ -52,6 +53,8 @@ export default class ProjectStatsFormatter extends Base {
|
||||
percentageReviewedFormat = chalk.green(percentageReviewedString);
|
||||
} else if (percentageReviewed > 100 / 2) {
|
||||
percentageReviewedFormat = chalk.yellow(percentageReviewedString);
|
||||
} else if (percentageReviewed <= 0) {
|
||||
percentageReviewedFormat = chalk.dim('No strings');
|
||||
} else {
|
||||
percentageReviewedFormat = chalk.red(percentageReviewedString);
|
||||
}
|
||||
|
@ -5,10 +5,10 @@ import fetch from 'node-fetch';
|
||||
|
||||
// Types
|
||||
import {Config} from '../types/config';
|
||||
import {Project} from '../types/project';
|
||||
import {ProjectViewer} from '../types/project';
|
||||
|
||||
export default class ProjectFetcher {
|
||||
async fetch(config: Config): Promise<Project> {
|
||||
async fetch(config: Config): Promise<ProjectViewer> {
|
||||
const response = await this.graphql(config);
|
||||
try {
|
||||
const data = await response.json();
|
||||
@ -20,7 +20,7 @@ export default class ProjectFetcher {
|
||||
);
|
||||
}
|
||||
|
||||
return data.data && data.data.viewer.project;
|
||||
return data.data && data.data.viewer;
|
||||
} catch (_) {
|
||||
throw new CLIError(
|
||||
chalk.red(`Can not fetch the project on ${config.apiUrl}`),
|
||||
@ -32,6 +32,10 @@ export default class ProjectFetcher {
|
||||
private async graphql(config: Config) {
|
||||
const query = `query ProjectDetails($project_id: ID!) {
|
||||
viewer {
|
||||
user {
|
||||
fullname
|
||||
}
|
||||
|
||||
project(id: $project_id) {
|
||||
id
|
||||
name
|
||||
|
@ -43,6 +43,13 @@ export interface Paginated<T> {
|
||||
meta: DocumentsMeta;
|
||||
}
|
||||
|
||||
export interface ProjectViewer {
|
||||
user: {
|
||||
fullname: string;
|
||||
};
|
||||
project: Project;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
|
@ -16,7 +16,11 @@ defmodule Accent do
|
||||
{:ok, _} = Logger.add_backend(Sentry.LoggerBackend)
|
||||
end
|
||||
|
||||
Ecto.DevLogger.install(Accent.Repo)
|
||||
Ecto.DevLogger.install(Accent.Repo,
|
||||
ignore_event: fn metadata ->
|
||||
not is_nil(metadata[:options][:telemetry_ui_conf])
|
||||
end
|
||||
)
|
||||
|
||||
opts = [strategy: :one_for_one, name: Accent.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
|
57
lib/accent/api_tokens/api_token_manager.ex
Normal file
57
lib/accent/api_tokens/api_token_manager.ex
Normal file
@ -0,0 +1,57 @@
|
||||
defmodule Accent.APITokenManager do
|
||||
alias Accent.RoleAbilities
|
||||
alias Accent.{AccessToken, Collaborator, Repo}
|
||||
alias Ecto.Multi
|
||||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
def create(project, user, params) do
|
||||
params = %{
|
||||
"custom_permissions" => params[:permissions],
|
||||
"user" => %{
|
||||
"fullname" => params[:name],
|
||||
"picture_url" => params[:picture_url]
|
||||
}
|
||||
}
|
||||
|
||||
changeset =
|
||||
%AccessToken{
|
||||
token: Accent.Utils.SecureRandom.urlsafe_base64(70)
|
||||
}
|
||||
|> cast(params, [:custom_permissions])
|
||||
|> validate_subset(:custom_permissions, Enum.map(RoleAbilities.actions_for(:all), &to_string/1))
|
||||
|> cast_assoc(:user,
|
||||
with: fn changeset, params ->
|
||||
changeset
|
||||
|> cast(params, [:fullname, :picture_url])
|
||||
|> put_change(:bot, true)
|
||||
end
|
||||
)
|
||||
|
||||
Multi.new()
|
||||
|> Multi.insert(:access_token, changeset)
|
||||
|> Multi.insert(:collaborator, fn %{access_token: access_token} ->
|
||||
%Collaborator{user_id: access_token.user_id, role: "bot", assigner_id: user.id, project_id: project.id}
|
||||
end)
|
||||
|> Repo.transaction()
|
||||
end
|
||||
|
||||
def revoke(access_token) do
|
||||
Repo.delete_all(from(AccessToken, where: [id: ^access_token.id]))
|
||||
:ok
|
||||
end
|
||||
|
||||
def list(project) do
|
||||
Repo.all(
|
||||
from(
|
||||
access_token in AccessToken,
|
||||
inner_join: user in assoc(access_token, :user),
|
||||
inner_join: collaboration in assoc(user, :collaborations),
|
||||
where: collaboration.project_id == ^project.id,
|
||||
where: user.bot == true,
|
||||
where: is_nil(access_token.revoked_at)
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
@ -1,4 +1,6 @@
|
||||
defmodule Accent.RoleAbilities do
|
||||
require Logger
|
||||
|
||||
alias Accent.MachineTranslations
|
||||
|
||||
@owner_role "owner"
|
||||
@ -7,27 +9,17 @@ defmodule Accent.RoleAbilities do
|
||||
@developer_role "developer"
|
||||
@reviewer_role "reviewer"
|
||||
|
||||
@any_actions ~w(
|
||||
@read_actions ~w(
|
||||
lint
|
||||
format
|
||||
index_permissions
|
||||
index_versions
|
||||
create_version
|
||||
update_version
|
||||
export_version
|
||||
index_translations
|
||||
index_comments
|
||||
create_comment
|
||||
delete_comment
|
||||
update_comment
|
||||
show_comment
|
||||
correct_all_revision
|
||||
uncorrect_all_revision
|
||||
show_project
|
||||
index_collaborators
|
||||
correct_translation
|
||||
uncorrect_translation
|
||||
update_translation
|
||||
show_translation
|
||||
index_project_activities
|
||||
index_related_translations
|
||||
@ -38,17 +30,30 @@ defmodule Accent.RoleAbilities do
|
||||
show_activity
|
||||
index_translation_activities
|
||||
index_translation_comments_subscriptions
|
||||
)a
|
||||
|
||||
@write_actions ~w(
|
||||
create_comment
|
||||
delete_comment
|
||||
update_comment
|
||||
correct_all_revision
|
||||
uncorrect_all_revision
|
||||
correct_translation
|
||||
uncorrect_translation
|
||||
update_translation
|
||||
create_translation_comments_subscription
|
||||
delete_translation_comments_subscription
|
||||
)a
|
||||
|
||||
@any_actions @read_actions ++ @write_actions
|
||||
|
||||
@bot_actions ~w(
|
||||
peek_sync
|
||||
peek_merge
|
||||
merge
|
||||
sync
|
||||
hook_update
|
||||
)a ++ @any_actions
|
||||
)a ++ @read_actions
|
||||
|
||||
@developer_actions ~w(
|
||||
peek_sync
|
||||
@ -57,11 +62,15 @@ defmodule Accent.RoleAbilities do
|
||||
sync
|
||||
delete_document
|
||||
update_document
|
||||
show_project_access_token
|
||||
list_project_api_tokens
|
||||
create_project_api_token
|
||||
revoke_project_api_token
|
||||
index_project_integrations
|
||||
create_project_integration
|
||||
update_project_integration
|
||||
delete_project_integration
|
||||
create_version
|
||||
update_version
|
||||
)a ++ @any_actions
|
||||
|
||||
@admin_actions ~w(
|
||||
@ -78,8 +87,10 @@ defmodule Accent.RoleAbilities do
|
||||
delete_project
|
||||
)a ++ @developer_actions
|
||||
|
||||
@configurable_actions ~w(machine_translations_translate_document achine_translations_translate_file machine_translations_translate_text)a
|
||||
@configurable_actions ~w(machine_translations_translate_document machine_translations_translate_file machine_translations_translate_text)a
|
||||
|
||||
def actions_for(:all), do: add_configurable_actions(@admin_actions, @owner_role)
|
||||
def actions_for({:custom, permissions}), do: permissions
|
||||
def actions_for(@owner_role), do: add_configurable_actions(@admin_actions, @owner_role)
|
||||
def actions_for(@admin_role), do: add_configurable_actions(@admin_actions, @admin_role)
|
||||
def actions_for(@bot_role), do: add_configurable_actions(@bot_actions, @bot_role)
|
||||
@ -96,17 +107,27 @@ defmodule Accent.RoleAbilities do
|
||||
MachineTranslations.translate_list_enabled?()
|
||||
end
|
||||
|
||||
def can?(_role, :machine_translations_translate_document), do: false
|
||||
|
||||
def can?(role, :machine_translations_translate_file) when role in [@owner_role, @admin_role, @developer_role] do
|
||||
MachineTranslations.translate_list_enabled?()
|
||||
end
|
||||
|
||||
def can?(_role, :machine_translations_translate_file), do: false
|
||||
|
||||
def can?(role, :machine_translations_translate_text) when role in [@owner_role, @admin_role, @developer_role] do
|
||||
MachineTranslations.translate_text_enabled?()
|
||||
end
|
||||
|
||||
def can?(_role, :machine_translations_translate_text), do: false
|
||||
|
||||
# Define abilities function at compile time to remove list lookup at runtime
|
||||
def can?(@owner_role, _action), do: true
|
||||
|
||||
def can?({:custom, permissions}, action) do
|
||||
to_string(action) in permissions
|
||||
end
|
||||
|
||||
for action <- @admin_actions do
|
||||
def can?(@admin_role, unquote(action)), do: true
|
||||
end
|
||||
@ -124,5 +145,10 @@ defmodule Accent.RoleAbilities do
|
||||
end
|
||||
|
||||
# Fallback if no permission has been found for the user on the project
|
||||
def can?(_role, _action), do: false
|
||||
def can?(nil, _action), do: false
|
||||
|
||||
def can?(role, action) do
|
||||
Logger.warn("Unauthorized action: #{action} for #{role}")
|
||||
false
|
||||
end
|
||||
end
|
||||
|
@ -21,17 +21,21 @@ defmodule Accent.UserAuthFetcher do
|
||||
from(
|
||||
user in User,
|
||||
inner_join: access_token in assoc(user, :access_tokens),
|
||||
left_join: collaboration in assoc(user, :bot_collaborations),
|
||||
where: access_token.token == ^token,
|
||||
where: is_nil(access_token.revoked_at)
|
||||
where: is_nil(access_token.revoked_at),
|
||||
select: {user, access_token.custom_permissions, collaboration}
|
||||
)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
defp fetch_user(_any), do: nil
|
||||
|
||||
defp map_permissions(nil), do: nil
|
||||
defp map_permissions({user, [_ | _] = permissions, %{project_id: id}}) do
|
||||
%{user | permissions: %{id => {:custom, permissions}}}
|
||||
end
|
||||
|
||||
defp map_permissions(user) do
|
||||
defp map_permissions({user, _, _}) do
|
||||
permissions =
|
||||
from(
|
||||
collaborator in Collaborator,
|
||||
@ -43,4 +47,6 @@ defmodule Accent.UserAuthFetcher do
|
||||
|
||||
%{user | permissions: permissions}
|
||||
end
|
||||
|
||||
defp map_permissions(nil), do: nil
|
||||
end
|
||||
|
@ -22,12 +22,15 @@ defmodule Accent.UserRemote.Authenticator do
|
||||
%User{
|
||||
provider: to_string(provider),
|
||||
fullname: info.name,
|
||||
picture_url: info.image,
|
||||
picture_url: normalize_picture_url(info.image, provider),
|
||||
email: normalize_email(info.email),
|
||||
uid: normalize_email(info.email)
|
||||
}
|
||||
end
|
||||
|
||||
defp normalize_picture_url("https://lh3.googleusercontent.com/a/default-user" <> _, :google), do: nil
|
||||
defp normalize_picture_url(url, _provider), do: url
|
||||
|
||||
defp normalize_email(email) do
|
||||
String.downcase(email)
|
||||
end
|
||||
|
@ -49,7 +49,7 @@ defmodule Accent.Endpoint do
|
||||
end
|
||||
|
||||
# sobelow_skip ["XSS.SendResp"]
|
||||
defp ping(%{request_path: "/ping"} = conn, _opts) do
|
||||
defp ping(conn = %{request_path: "/ping"}, _opts) do
|
||||
alias Plug.Conn
|
||||
version = Application.get_env(:accent, :version)
|
||||
|
||||
@ -61,7 +61,7 @@ defmodule Accent.Endpoint do
|
||||
|
||||
defp ping(conn, _opts), do: conn
|
||||
|
||||
defp canonical_host(%{request_path: "/health"} = conn, _opts), do: conn
|
||||
defp canonical_host(conn = %{request_path: "/health"}, _opts), do: conn
|
||||
|
||||
defp canonical_host(conn, _opts) do
|
||||
opts = PlugCanonicalHost.init(canonical_host: Application.get_env(:accent, :canonical_host))
|
||||
|
@ -3,7 +3,7 @@ defmodule Accent.ProjectDeleter do
|
||||
|
||||
def delete(project: project) do
|
||||
project
|
||||
|> Ecto.assoc(:collaborators)
|
||||
|> Ecto.assoc(:all_collaborators)
|
||||
|> Repo.delete_all()
|
||||
|
||||
{:ok, project}
|
||||
|
@ -5,6 +5,7 @@ defmodule Accent.AccessToken do
|
||||
field(:token, :string)
|
||||
field(:global, :boolean)
|
||||
field(:revoked_at, :naive_datetime)
|
||||
field(:custom_permissions, {:array, :string})
|
||||
|
||||
belongs_to(:user, Accent.User)
|
||||
|
||||
|
@ -17,7 +17,8 @@ defmodule Accent.Project do
|
||||
has_many(:target_revisions, Accent.Revision, where: [master: false])
|
||||
has_many(:versions, Accent.Version)
|
||||
has_many(:operations, Accent.Operation)
|
||||
has_many(:collaborators, Accent.Collaborator)
|
||||
has_many(:collaborators, Accent.Collaborator, where: [role: {:in, ["reviewer", "admin", "developer", "owner"]}])
|
||||
has_many(:all_collaborators, Accent.Collaborator)
|
||||
belongs_to(:language, Accent.Language)
|
||||
|
||||
timestamps()
|
||||
|
@ -12,6 +12,7 @@ defmodule Accent.User do
|
||||
has_many(:private_access_tokens, Accent.AccessToken, where: [global: false])
|
||||
has_many(:auth_providers, Accent.AuthProvider)
|
||||
has_many(:collaborations, Accent.Collaborator)
|
||||
has_many(:bot_collaborations, Accent.Collaborator, where: [role: "bot"])
|
||||
has_many(:collaboration_assigns, Accent.Collaborator, foreign_key: :assigner_id)
|
||||
|
||||
field(:permissions, :map, virtual: true)
|
||||
|
@ -83,14 +83,12 @@ defmodule Accent.TelemetryUI do
|
||||
description: "Database query total time",
|
||||
keep: ecto_keep,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [report_as: "ecto"],
|
||||
ui_options: [class: "col-span-3", unit: " ms"]
|
||||
),
|
||||
average_over_time("accent.repo.query.total_time",
|
||||
description: "Database query total time over time",
|
||||
keep: ecto_keep,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [report_as: "ecto"],
|
||||
ui_options: [class: "col-span-5", unit: " ms"]
|
||||
),
|
||||
average("accent.repo.query.total_time",
|
||||
@ -98,7 +96,6 @@ defmodule Accent.TelemetryUI do
|
||||
keep: ecto_keep,
|
||||
tags: [:source],
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [report_as: "ecto"],
|
||||
ui_options: [class: "col-span-full", unit: " ms"]
|
||||
)
|
||||
]
|
||||
@ -119,28 +116,24 @@ defmodule Accent.TelemetryUI do
|
||||
average("absinthe.execute.operation.stop.duration",
|
||||
description: "Absinthe operation duration",
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [report_as: "absinthe"],
|
||||
ui_options: [class: "col-span-3", unit: " ms"]
|
||||
),
|
||||
average_over_time("absinthe.execute.operation.stop.duration",
|
||||
description: "Absinthe operation duration over time",
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [report_as: "absinthe"],
|
||||
ui_options: [class: "col-span-5", unit: " ms"]
|
||||
),
|
||||
counter("absinthe.execute.operation.stop.duration",
|
||||
description: "Count Absinthe executions per operation",
|
||||
tags: [:operation_name],
|
||||
tag_values: absinthe_tag_values,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [report_as: "absinthe"]
|
||||
unit: {:native, :millisecond}
|
||||
),
|
||||
average_over_time("absinthe.execute.operation.stop.duration",
|
||||
description: "Absinthe duration per operation",
|
||||
tags: [:operation_name],
|
||||
tag_values: absinthe_tag_values,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [report_as: "absinthe"]
|
||||
unit: {:native, :millisecond}
|
||||
)
|
||||
]
|
||||
end
|
||||
@ -165,65 +158,69 @@ defmodule Accent.TelemetryUI do
|
||||
end
|
||||
|
||||
[
|
||||
counter("phoenix.router_dispatch.stop.duration",
|
||||
counter("graphql.router_dispatch.duration",
|
||||
event_name: [:phoenix, :router_dispatch, :stop],
|
||||
description: "Number of GraphQL requests",
|
||||
keep: graphql_keep,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [report_as: "graphql"],
|
||||
ui_options: [class: "col-span-3", unit: " requests"]
|
||||
),
|
||||
count_over_time("phoenix.router_dispatch.stop.duration",
|
||||
count_over_time("graphql.router_dispatch.duration",
|
||||
event_name: [:phoenix, :router_dispatch, :stop],
|
||||
description: "Number of GraphQL requests over time",
|
||||
keep: graphql_keep,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [report_as: "graphql"],
|
||||
ui_options: [class: "col-span-5", unit: " requests"]
|
||||
),
|
||||
average("phoenix.router_dispatch.stop.duration",
|
||||
average("graphql.router_dispatch.duration",
|
||||
event_name: [:phoenix, :router_dispatch, :stop],
|
||||
description: "GraphQL requests duration",
|
||||
keep: graphql_keep,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [report_as: "graphql"],
|
||||
ui_options: [class: "col-span-3", unit: " ms"]
|
||||
),
|
||||
average_over_time("phoenix.router_dispatch.stop.duration",
|
||||
average_over_time("graphql.router_dispatch.duration",
|
||||
event_name: [:phoenix, :router_dispatch, :stop],
|
||||
description: "GraphQL requests duration over time",
|
||||
keep: graphql_keep,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [report_as: "graphql"],
|
||||
ui_options: [class: "col-span-5", unit: " ms"]
|
||||
),
|
||||
count_over_time("phoenix.router_dispatch.stop.duration",
|
||||
count_over_time("graphql.router_dispatch.duration",
|
||||
event_name: [:phoenix, :router_dispatch, :stop],
|
||||
description: "GraphQL requests count per operation",
|
||||
keep: graphql_keep,
|
||||
tag_values: graphql_tag_values,
|
||||
tags: [:operation_name],
|
||||
unit: {:native, :millisecond},
|
||||
ui_options: [unit: " requests"],
|
||||
reporter_options: [report_as: "graphql", class: "col-span-4"]
|
||||
reporter_options: [class: "col-span-4"]
|
||||
),
|
||||
counter("phoenix.router_dispatch.stop.duration",
|
||||
counter("graphql.router_dispatch.duration",
|
||||
event_name: [:phoenix, :router_dispatch, :stop],
|
||||
description: "Count GraphQL requests by operation",
|
||||
keep: graphql_keep,
|
||||
tag_values: graphql_tag_values,
|
||||
tags: [:operation_name],
|
||||
unit: {:native, :millisecond},
|
||||
ui_options: [unit: " requests"],
|
||||
reporter_options: [report_as: "graphql", class: "col-span-4"]
|
||||
reporter_options: [class: "col-span-4"]
|
||||
),
|
||||
average("phoenix.router_dispatch.stop.duration",
|
||||
average("graphql.router_dispatch.duration",
|
||||
event_name: [:phoenix, :router_dispatch, :stop],
|
||||
description: "GraphQL requests duration per operation",
|
||||
keep: graphql_keep,
|
||||
tag_values: graphql_tag_values,
|
||||
tags: [:operation_name],
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [report_as: "graphql", class: "col-span-4"]
|
||||
reporter_options: [class: "col-span-4"]
|
||||
),
|
||||
distribution("phoenix.router_dispatch.stop.duration",
|
||||
distribution("graphql.router_dispatch.duration",
|
||||
event_name: [:phoenix, :router_dispatch, :stop],
|
||||
description: "GraphQL requests duration",
|
||||
keep: graphql_keep,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [report_as: "graphql", buckets: [0, 100, 500, 2000]]
|
||||
reporter_options: [buckets: [0, 100, 500, 2000]]
|
||||
)
|
||||
]
|
||||
end
|
||||
|
20
lib/graphql/error_reporting.ex
Normal file
20
lib/graphql/error_reporting.ex
Normal file
@ -0,0 +1,20 @@
|
||||
defmodule Accent.GraphQL.ErrorReporting do
|
||||
require Logger
|
||||
|
||||
def run(%{result: %{errors: errors}, source: source} = blueprint, _) when not is_nil(errors) do
|
||||
Logger.error("""
|
||||
#{operation_name(Absinthe.Blueprint.current_operation(blueprint))}
|
||||
Errors: #{inspect(errors)}
|
||||
Source: #{inspect(source)}
|
||||
""")
|
||||
|
||||
{:ok, blueprint}
|
||||
end
|
||||
|
||||
def run(blueprint, _) do
|
||||
{:ok, blueprint}
|
||||
end
|
||||
|
||||
defp operation_name(nil), do: nil
|
||||
defp operation_name(operation), do: operation.name
|
||||
end
|
@ -1,6 +1,8 @@
|
||||
defmodule Accent.GraphQL.Helpers.Authorization do
|
||||
import Accent.GraphQL.Plugins.Authorization
|
||||
|
||||
alias Accent.AccessToken
|
||||
|
||||
alias Accent.{
|
||||
Collaborator,
|
||||
Comment,
|
||||
@ -140,6 +142,19 @@ defmodule Accent.GraphQL.Helpers.Authorization do
|
||||
end
|
||||
end
|
||||
|
||||
def api_token_authorize(action, func) do
|
||||
fn _, args, info ->
|
||||
access_token =
|
||||
AccessToken
|
||||
|> Repo.get(args.id)
|
||||
|> Repo.preload(user: :collaborations)
|
||||
|
||||
project_id = List.first(access_token.user.collaborations).project_id
|
||||
|
||||
authorize(action, project_id, info, do: func.(access_token, args, info))
|
||||
end
|
||||
end
|
||||
|
||||
def integration_authorize(action, func) do
|
||||
fn _, args, info ->
|
||||
integration =
|
||||
|
24
lib/graphql/mutations/api_token.ex
Normal file
24
lib/graphql/mutations/api_token.ex
Normal file
@ -0,0 +1,24 @@
|
||||
defmodule Accent.GraphQL.Mutations.APIToken do
|
||||
use Absinthe.Schema.Notation
|
||||
|
||||
import Accent.GraphQL.Helpers.Authorization
|
||||
|
||||
alias Accent.GraphQL.Resolvers.APIToken, as: APITokenResolver
|
||||
|
||||
object :api_token_mutations do
|
||||
field :create_api_token, :mutated_api_token do
|
||||
arg(:project_id, non_null(:id))
|
||||
arg(:name, non_null(:string))
|
||||
arg(:picture_url, :string)
|
||||
arg(:permissions, list_of(non_null(:string)))
|
||||
|
||||
resolve(project_authorize(:create_project_api_token, &APITokenResolver.create/3, :project_id))
|
||||
end
|
||||
|
||||
field :revoke_api_token, :boolean do
|
||||
arg(:id, non_null(:id))
|
||||
|
||||
resolve(api_token_authorize(:revoke_project_api_token, &APITokenResolver.revoke/3))
|
||||
end
|
||||
end
|
||||
end
|
@ -1,26 +0,0 @@
|
||||
defmodule Accent.GraphQL.Resolvers.AccessToken do
|
||||
import Ecto.Query, only: [from: 2]
|
||||
|
||||
alias Accent.{
|
||||
AccessToken,
|
||||
Plugs.GraphQLContext,
|
||||
Project,
|
||||
Repo
|
||||
}
|
||||
|
||||
@spec show_project(Project.t(), any(), GraphQLContext.t()) :: {:ok, AccessToken.t() | nil}
|
||||
def show_project(project, _, _) do
|
||||
from(
|
||||
access_token in AccessToken,
|
||||
inner_join: user in assoc(access_token, :user),
|
||||
inner_join: collaboration in assoc(user, :collaborations),
|
||||
where: collaboration.project_id == ^project.id,
|
||||
where: user.bot == true
|
||||
)
|
||||
|> Repo.one()
|
||||
|> case do
|
||||
%AccessToken{token: token} -> {:ok, token}
|
||||
_ -> {:ok, nil}
|
||||
end
|
||||
end
|
||||
end
|
30
lib/graphql/resolvers/api_token.ex
Normal file
30
lib/graphql/resolvers/api_token.ex
Normal file
@ -0,0 +1,30 @@
|
||||
defmodule Accent.GraphQL.Resolvers.APIToken do
|
||||
alias Accent.{
|
||||
AccessToken,
|
||||
APITokenManager,
|
||||
Plugs.GraphQLContext,
|
||||
Project
|
||||
}
|
||||
|
||||
@spec create(Project.t(), any(), GraphQLContext.t()) :: {:ok, AccessToken.t() | nil}
|
||||
def create(project, args, info) do
|
||||
case APITokenManager.create(project, info.context[:conn].assigns[:current_user], args) do
|
||||
{:ok, %{access_token: api_token}} ->
|
||||
{:ok, %{api_token: api_token, errors: nil}}
|
||||
|
||||
{:error, _reason, _, _} ->
|
||||
{:ok, %{access_token: nil, errors: ["unprocessable_entity"]}}
|
||||
end
|
||||
end
|
||||
|
||||
@spec revoke(Project.t(), any(), GraphQLContext.t()) :: {:ok, AccessToken.t() | nil}
|
||||
def revoke(access_token, _args, _) do
|
||||
APITokenManager.revoke(access_token)
|
||||
{:ok, true}
|
||||
end
|
||||
|
||||
@spec list_project(Project.t(), any(), GraphQLContext.t()) :: {:ok, AccessToken.t() | nil}
|
||||
def list_project(project, _, _) do
|
||||
{:ok, APITokenManager.list(project)}
|
||||
end
|
||||
end
|
@ -8,6 +8,7 @@ defmodule Accent.GraphQL.Schema do
|
||||
|
||||
# Types
|
||||
import_types(AbsintheErrorPayload.ValidationMessageTypes)
|
||||
import_types(Accent.GraphQL.Types.APIToken)
|
||||
import_types(Accent.GraphQL.Types.AuthenticationProvider)
|
||||
import_types(Accent.GraphQL.Types.DocumentFormat)
|
||||
import_types(Accent.GraphQL.Types.Role)
|
||||
@ -54,6 +55,7 @@ defmodule Accent.GraphQL.Schema do
|
||||
|
||||
mutation do
|
||||
# Mutation types
|
||||
import_types(Accent.GraphQL.Mutations.APIToken)
|
||||
import_types(Accent.GraphQL.Mutations.Translation)
|
||||
import_types(Accent.GraphQL.Mutations.Comment)
|
||||
import_types(Accent.GraphQL.Mutations.Collaborator)
|
||||
@ -64,6 +66,7 @@ defmodule Accent.GraphQL.Schema do
|
||||
import_types(Accent.GraphQL.Mutations.Operation)
|
||||
import_types(Accent.GraphQL.Mutations.Version)
|
||||
|
||||
import_fields(:api_token_mutations)
|
||||
import_fields(:comment_mutations)
|
||||
import_fields(:translation_mutations)
|
||||
import_fields(:collaborator_mutations)
|
||||
@ -107,4 +110,13 @@ defmodule Accent.GraphQL.Schema do
|
||||
def middleware(middleware, _, _) do
|
||||
[NewRelic.Absinthe.Middleware] ++ middleware
|
||||
end
|
||||
|
||||
def absinthe_pipeline(config, opts) do
|
||||
config
|
||||
|> Absinthe.Plug.default_pipeline(opts)
|
||||
|> Absinthe.Pipeline.insert_after(
|
||||
Absinthe.Phase.Document.Result,
|
||||
Accent.GraphQL.ErrorReporting
|
||||
)
|
||||
end
|
||||
end
|
||||
|
13
lib/graphql/types/api_token.ex
Normal file
13
lib/graphql/types/api_token.ex
Normal file
@ -0,0 +1,13 @@
|
||||
defmodule Accent.GraphQL.Types.APIToken do
|
||||
use Absinthe.Schema.Notation
|
||||
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
|
||||
object :api_token do
|
||||
field(:id, non_null(:id))
|
||||
field(:token, non_null(:id))
|
||||
field(:custom_permissions, list_of(non_null(:string)))
|
||||
field(:user, non_null(:user), resolve: dataloader(Accent.User))
|
||||
field(:inserted_at, non_null(:datetime))
|
||||
end
|
||||
end
|
@ -21,6 +21,11 @@ defmodule Accent.GraphQL.Types.MutationResult do
|
||||
field(:errors, list_of(:string))
|
||||
end
|
||||
|
||||
object :mutated_api_token do
|
||||
field(:api_token, :api_token)
|
||||
field(:errors, list_of(:string))
|
||||
end
|
||||
|
||||
object :mutated_collaborator do
|
||||
field(:collaborator, :collaborator)
|
||||
field(:errors, list_of(:string))
|
||||
|
@ -45,8 +45,8 @@ defmodule Accent.GraphQL.Types.Project do
|
||||
resolve(project_authorize(:lint, &Accent.GraphQL.Resolvers.Project.lint_translations/3))
|
||||
end
|
||||
|
||||
field :access_token, :string do
|
||||
resolve(project_authorize(:show_project_access_token, &Accent.GraphQL.Resolvers.AccessToken.show_project/3))
|
||||
field :api_tokens, list_of(non_null(:api_token)) do
|
||||
resolve(project_authorize(:list_project_api_tokens, &Accent.GraphQL.Resolvers.APIToken.list_project/3))
|
||||
end
|
||||
|
||||
field :viewer_permissions, list_of(:string) do
|
||||
|
@ -79,6 +79,13 @@ defmodule Movement.Persisters.Base do
|
||||
|> assign_revision(assigns[:revision])
|
||||
|> assign_version(assigns[:version])
|
||||
|
||||
placeholder_values =
|
||||
Map.new(
|
||||
Enum.map(placeholders, fn {key, _value} ->
|
||||
{key, {:placeholder, key}}
|
||||
end)
|
||||
)
|
||||
|
||||
operations =
|
||||
context.operations
|
||||
|> Stream.map(fn operation ->
|
||||
@ -86,12 +93,12 @@ defmodule Movement.Persisters.Base do
|
||||
operation
|
||||
| inserted_at: {:placeholder, :now},
|
||||
updated_at: {:placeholder, :now},
|
||||
user_id: {:placeholder, :user_id},
|
||||
document_id: {:placeholder, :document_id},
|
||||
project_id: {:placeholder, :project_id},
|
||||
batch_operation_id: {:placeholder, :batch_operation_id},
|
||||
version_id: operation.version_id || {:placeholder, :version_id},
|
||||
revision_id: operation.revision_id || {:placeholder, :revision_id}
|
||||
user_id: placeholder_values[:user_id] || operation.user_id,
|
||||
document_id: placeholder_values[:document_id] || operation.document_id,
|
||||
project_id: placeholder_values[:project_id] || operation.project_id,
|
||||
batch_operation_id: placeholder_values[:batch_operation_id] || operation.batch_operation_id,
|
||||
version_id: operation.version_id || placeholder_values[:version_id],
|
||||
revision_id: operation.revision_id || placeholder_values[:revision_id]
|
||||
})
|
||||
end)
|
||||
|> Stream.chunk_every(@operations_inserts_chunk)
|
||||
@ -127,18 +134,28 @@ defmodule Movement.Persisters.Base do
|
||||
{context, Migrator.down([operation])}
|
||||
end
|
||||
|
||||
defp assign_project(placeholders, nil), do: placeholders
|
||||
|
||||
defp assign_project(placeholders, project),
|
||||
do: Map.put(placeholders, :project_id, project && project.id)
|
||||
|
||||
defp assign_batch_operation(placeholders, nil), do: placeholders
|
||||
|
||||
defp assign_batch_operation(placeholders, batch_operation),
|
||||
do: Map.put(placeholders, :batch_operation_id, batch_operation && batch_operation.id)
|
||||
|
||||
defp assign_document(placeholders, nil), do: placeholders
|
||||
|
||||
defp assign_document(placeholders, document),
|
||||
do: Map.put(placeholders, :document_id, document && document.id)
|
||||
|
||||
defp assign_revision(placeholders, nil), do: placeholders
|
||||
|
||||
defp assign_revision(placeholders, revision),
|
||||
do: Map.put(placeholders, :revision_id, revision && revision.id)
|
||||
|
||||
defp assign_version(placeholders, nil), do: placeholders
|
||||
|
||||
defp assign_version(placeholders, version),
|
||||
do: Map.put(placeholders, :version_id, version && version.id)
|
||||
|
||||
|
@ -19,7 +19,7 @@ defmodule Accent.Router do
|
||||
scope "/graphql" do
|
||||
pipe_through(:graphql)
|
||||
|
||||
forward("/", Absinthe.Plug, schema: Accent.GraphQL.Schema)
|
||||
forward("/", Absinthe.Plug, schema: Accent.GraphQL.Schema, pipeline: {Accent.GraphQL.Schema, :absinthe_pipeline})
|
||||
end
|
||||
|
||||
pipeline :authenticate do
|
||||
|
4
mix.exs
4
mix.exs
@ -94,13 +94,13 @@ defmodule Accent.Mixfile do
|
||||
{:bamboo_smtp, "~> 2.0"},
|
||||
|
||||
# Events handling
|
||||
{:oban, "~> 2.0"},
|
||||
{:oban, "~> 2.13.0"},
|
||||
|
||||
# Metrics and monitoring
|
||||
{:new_relic_agent, "~> 1.27"},
|
||||
{:new_relic_absinthe, "~> 0.0"},
|
||||
{:telemetry, "~> 1.0", override: true},
|
||||
{:telemetry_ui, "~> 1.1"},
|
||||
{:telemetry_ui, path: "../mirego/telemetry_ui"},
|
||||
|
||||
# Mock testing
|
||||
{:mox, "~> 0.3", only: :test},
|
||||
|
6
mix.lock
6
mix.lock
@ -7,7 +7,7 @@
|
||||
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
|
||||
"canada": {:hex, :canada, "1.0.2", "040e4c47609b0a67d5773ac1fbe5e99f840cef173d69b739beda7c98453e0770", [:mix], [], "hexpm", "4269f74153fe89583fe50bd4d5de57bfe01f31258a6b676d296f3681f1483c68"},
|
||||
"canary": {:hex, :canary, "1.1.1", "4138d5e05db8497c477e4af73902eb9ae06e49dceaa13c2dd9f0b55525ded48b", [:mix], [{:canada, "~> 1.0.1", [hex: :canada, repo: "hexpm", optional: false]}, {:ecto, ">= 1.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f348d9848693c830a65b707bba9e4dfdd6434e8c356a8d4477e4535afb0d653b"},
|
||||
"castore": {:hex, :castore, "0.1.20", "62a0126cbb7cb3e259257827b9190f88316eb7aa3fdac01fd6f2dfd64e7f46e9", [:mix], [], "hexpm", "a020b7650529c986c454a4035b6b13a328e288466986307bea3aadb4c95ac98a"},
|
||||
"castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},
|
||||
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
|
||||
@ -59,7 +59,7 @@
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.4", "7b06aefe43efbbee7f33fa0875b409400243e4c96d7bb2f962d0a0c0b5ddacb1", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b37778447c9d3e9904899e132d62a7d484fa8abcc10433b1b820a9b907b82852"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.11", "c50eac83dae6b5488859180422dfb27b2c609de87f4aa5b9c926ecd0501cd44f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "76c99a0ffb47cd95bf06a917e74f282a603f3e77b00375f3c2dd95110971b102"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.0", "c57bc5044f25f007dc86ab21895688c098a9f846a8dda6bc40e2d0ddc146e38f", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "1b066f99a26fd22064c12b2600a9a6e56700f591bf7b20b418054ea38b4d4357"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
|
||||
@ -76,7 +76,7 @@
|
||||
"sentry": {:hex, :sentry, "7.2.5", "570db92c3bbacd6ad02ac81cba8ac5af11235a55d65ac4375e3ec833975b83d3", [:mix], [{:hackney, "~> 1.8 or 1.6.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "ea84ed6848505ff2a246567df562f465d2b34c317d3ecba7c7df58daa56e5e5d"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
||||
"table": {:hex, :table, "0.1.2", "87ad1125f5b70c5dea0307aa633194083eb5182ec537efc94e96af08937e14a8", [:mix], [], "hexpm", "7e99bc7efef806315c7e65640724bf165c3061cdc5d854060f74468367065029"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.0", "a8ce551485a9a3dac8d523542de130eafd12e40bbf76cf0ecd2528f24e812a44", [:rebar3], [], "hexpm", "1427e73667b9a2002cf1f26694c422d5c905df889023903c4518921d53e3e883"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
|
||||
"telemetry_ui": {:hex, :telemetry_ui, "1.2.0", "87ab95991e876966584d5071476171782ed0adef839724cd465bdbb35c8ebc99", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, "~> 2.13", [hex: :oban, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_ecto, "~> 4.4", [hex: :phoenix_ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}, {:timex, "~> 3.7", [hex: :timex, repo: "hexpm", optional: false]}, {:vega_lite, "~> 0.1", [hex: :vega_lite, repo: "hexpm", optional: false]}], "hexpm", "60613da7d689e68fa36bccb63feb457f3276ca9ecea4342e5ba0e6ccb699406f"},
|
||||
|
@ -0,0 +1,11 @@
|
||||
defmodule Accent.Repo.Migrations.UpgradeTelemetryUi2 do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
TelemetryUI.Backend.EctoPostgres.Migrations.up(version: 3)
|
||||
end
|
||||
|
||||
def down do
|
||||
TelemetryUI.Backend.EctoPostgres.Migrations.down(version: 2)
|
||||
end
|
||||
end
|
@ -0,0 +1,9 @@
|
||||
defmodule Accent.Repo.Migrations.AddCustomPermissionsOnTokens do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:auth_access_tokens) do
|
||||
add(:custom_permissions, {:array, :string})
|
||||
end
|
||||
end
|
||||
end
|
@ -1,22 +0,0 @@
|
||||
defmodule AccentTest.GraphQL.Resolvers.AccessToken do
|
||||
use Accent.RepoCase
|
||||
|
||||
alias Accent.GraphQL.Resolvers.AccessToken, as: Resolver
|
||||
|
||||
alias Accent.{
|
||||
AccessToken,
|
||||
Collaborator,
|
||||
Project,
|
||||
Repo,
|
||||
User
|
||||
}
|
||||
|
||||
test "show project" do
|
||||
user = %User{email: "test@example.com", bot: true} |> Repo.insert!()
|
||||
project = %Project{main_color: "#f00", name: "My project"} |> Repo.insert!()
|
||||
%Collaborator{project_id: project.id, user_id: user.id, role: "bot"} |> Repo.insert!()
|
||||
token = %AccessToken{user_id: user.id, token: "foo"} |> Repo.insert!()
|
||||
|
||||
assert Resolver.show_project(project, %{}, %{}) == {:ok, token.token}
|
||||
end
|
||||
end
|
@ -39,7 +39,6 @@ defmodule AccentTest.GraphQL.Resolvers.Permission do
|
||||
{:ok, permissions} = Resolver.list_project(project, %{}, context)
|
||||
|
||||
assert :create_slave in permissions
|
||||
assert :show_project_access_token in permissions
|
||||
assert :show_project in permissions
|
||||
end
|
||||
|
||||
|
@ -34,7 +34,7 @@ defmodule AccentTest.ProjectCreator do
|
||||
params = %{"main_color" => "#f00", "name" => "OK", "language_id" => language.id}
|
||||
|
||||
{:ok, project} = ProjectCreator.create(params: params, user: user)
|
||||
bot_collaborator = project |> Ecto.assoc(:collaborators) |> Ecto.Query.where([c], c.role == "bot") |> Repo.one()
|
||||
bot_collaborator = project |> Ecto.assoc(:all_collaborators) |> Ecto.Query.where([c], c.role == "bot") |> Repo.one()
|
||||
bot_user = Repo.preload(bot_collaborator, :user).user
|
||||
bot_access = Repo.preload(bot_collaborator, user: :access_tokens).user.access_tokens |> hd()
|
||||
|
||||
|
@ -5,15 +5,15 @@ defmodule AccentTest.ProjectDeleter do
|
||||
|
||||
test "create with language and user" do
|
||||
project = %Project{main_color: "#f00", name: "french"} |> Repo.insert!()
|
||||
collaborator = %Collaborator{project_id: project.id} |> Repo.insert!()
|
||||
collaborator = %Collaborator{project_id: project.id, role: "reviewer"} |> Repo.insert!()
|
||||
|
||||
assert project
|
||||
|> Ecto.assoc(:collaborators)
|
||||
|> Ecto.assoc(:all_collaborators)
|
||||
|> Repo.all()
|
||||
|> Enum.map(& &1.id) === [collaborator.id]
|
||||
|
||||
{:ok, project} = ProjectDeleter.delete(project: project)
|
||||
|
||||
assert Repo.all(Ecto.assoc(project, :collaborators)) === []
|
||||
assert Repo.all(Ecto.assoc(project, :all_collaborators)) === []
|
||||
end
|
||||
end
|
||||
|
@ -1,6 +1,12 @@
|
||||
import {isBlank} from '@ember/utils';
|
||||
import {helper} from '@ember/component/helper';
|
||||
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
|
||||
import {frCA, enUS} from 'date-fns/locale';
|
||||
|
||||
const LOCALES = {
|
||||
'fr-ca': frCA,
|
||||
'en-us': enUS,
|
||||
} as any;
|
||||
|
||||
const OPTIONS = {
|
||||
addSuffix: true,
|
||||
@ -9,8 +15,14 @@ const OPTIONS = {
|
||||
|
||||
const timeAgoInWords = ([date]: [string]) => {
|
||||
if (isBlank(date)) return '';
|
||||
const locale = LOCALES[localStorage.getItem('locale') || 'en-us'] || enUS;
|
||||
|
||||
return formatDistanceToNow(new Date(date), OPTIONS);
|
||||
const options = {
|
||||
locale,
|
||||
...OPTIONS,
|
||||
};
|
||||
|
||||
return formatDistanceToNow(new Date(date), options);
|
||||
};
|
||||
|
||||
export default helper(timeAgoInWords);
|
||||
|
@ -361,7 +361,7 @@
|
||||
"collaborators_text": "Manage who has access to what features of this project",
|
||||
"badges": "Badges",
|
||||
"badges_text": "Quick look on project stats, embedded in images",
|
||||
"api_token": "API Token",
|
||||
"api_token": "API Tokens",
|
||||
"api_token_text": "Integrate Accent workflow with the CLI or plain API requests",
|
||||
"service_integrations": "Service & integrations",
|
||||
"service_integrations_text": "Notify and interact with external systems based on Accent events",
|
||||
@ -400,13 +400,20 @@
|
||||
"use_language_text": "By switching to the Accent language using your favorite framework i18n tool, the page will display the strings in the pseudo language files and the Accent script will replace those by the strings in Accent. It will also bind click event on the elements so that you can select strings directly in your app and edit them via Accent."
|
||||
},
|
||||
"api_token": {
|
||||
"title": "API token",
|
||||
"text_1": "With this token, you can make authentified calls to Accent’s API. All operations will be flagged as \"made by the API client\".",
|
||||
"text_2": "Typically, this is used to sync and add translations the localization files in a deploy script."
|
||||
"title": "API tokens",
|
||||
"text_1": "With this token, you can make authentified calls to Accent’s API. All tokens have the 'bot' permissions: sync and add translations. You can specifiy custom permissions to have more fine grained tokens across your project. Set the token as <code>ACCENT_API_KEY</code> in your GitHub actions workflows to be authenticated using accent-cli.",
|
||||
"revoke_button": "Revoke",
|
||||
"revoke_confirm": "Are you sure you want to revoke the token? This operation can’t be reverted.",
|
||||
"inserted_at": "Created:",
|
||||
"create_title": "Add a new API token",
|
||||
"create_button": "Create",
|
||||
"create_name_placeholder": "Display name",
|
||||
"create_picture_url_placeholder": "Avatar URL (optional)"
|
||||
},
|
||||
"user_token": {
|
||||
"title": "Your personal token",
|
||||
"text_1": "You can also use your personal token to make API call as yourself. Beware, this token is global for all your project."
|
||||
"text_1": "You can also use your personal token to make API call as yourself. Beware, this token is global for all your project.",
|
||||
"text_shell": "You can set a global environment variable named <code>ACCENT_API_KEY</code> on your shell (bash, zsh, fish, etc) to be authenticated when you use accent-cli."
|
||||
},
|
||||
"badges": {
|
||||
"title": "Badges",
|
||||
@ -562,7 +569,7 @@
|
||||
"error": "Invalid project",
|
||||
"cancel_button": "Cancel",
|
||||
"language_label": "Master language:",
|
||||
"language_search_placeholder": "Search languages…",
|
||||
"language_search_placeholder": "Search…",
|
||||
"name_label": "Name:",
|
||||
"save_button": "Create"
|
||||
},
|
||||
@ -662,7 +669,7 @@
|
||||
"conflicted_label": "in review",
|
||||
"master_label": "master",
|
||||
"save_button": "Save",
|
||||
"last_updated_label": "Last updated: ",
|
||||
"last_updated_label": "Last updated:",
|
||||
"new_language_link": "New language",
|
||||
"no_related_translations": "No translations yet. You need to add another language to your project."
|
||||
},
|
||||
@ -671,7 +678,7 @@
|
||||
},
|
||||
"project_manage_languages_create_form": {
|
||||
"default_null": "Initialize language with empty strings",
|
||||
"language_search_placeholder": "Search languages…",
|
||||
"language_search_placeholder": "Search…",
|
||||
"save_button": "Add language"
|
||||
},
|
||||
"project_manage_languages_overview": {
|
||||
@ -825,7 +832,7 @@
|
||||
"in_review_label": "in review",
|
||||
"in_review_tooltip": "View in review",
|
||||
"lint_messages_label": "{count, plural, =0 {No linting warnings} =1 {1 linting warning} other {# linting warnings}}",
|
||||
"last_updated_label": "Last updated: ",
|
||||
"last_updated_label": "Last updated:",
|
||||
"maybe_sync_before": "Maybe try to",
|
||||
"maybe_sync_link": "sync some files →",
|
||||
"no_translations": "Looks like no strings were added for your project.",
|
||||
@ -843,7 +850,7 @@
|
||||
"manage_languages_text": "Target languages follow your master language strings and conflicts",
|
||||
"add_collaborator": "Add collaborator",
|
||||
"add_collaborator_text": "Translators, developers, etc.",
|
||||
"api_token": "Get your API Token",
|
||||
"api_token": "Get an API Token",
|
||||
"api_token_text": "Interact with the API from your project",
|
||||
"start_review": "Start to review and translate",
|
||||
"export": "Export the corrected file",
|
||||
@ -858,6 +865,7 @@
|
||||
"ADMIN": "Admin",
|
||||
"DEVELOPER": "Developer",
|
||||
"OWNER": "Owner",
|
||||
"BOT": "API Token",
|
||||
"REVIEWER": "Reviewer"
|
||||
},
|
||||
"integration_services": {
|
||||
@ -977,7 +985,11 @@
|
||||
"loading_content": "Fetching project’s badges…"
|
||||
},
|
||||
"api_token": {
|
||||
"loading_content": "Fetching project’s API settings…"
|
||||
"loading_content": "Fetching project’s API settings…",
|
||||
"api_token_add_success": "API token has been created with success",
|
||||
"api_token_add_error": "API token could not be created",
|
||||
"api_token_revoke_success": "API token has been revoked with success",
|
||||
"api_token_revoke_error": "API token could not be revoked"
|
||||
},
|
||||
"jipt": {
|
||||
"loading_content": "Fetching project’s Just In Place Translations settings…"
|
||||
|
1079
webapp/app/locales/fr-ca.json
Normal file
1079
webapp/app/locales/fr-ca.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,8 @@ export default class ApplicationRoute extends Route {
|
||||
router: RouterService;
|
||||
|
||||
async beforeModel() {
|
||||
this.intl.setLocale('en-us');
|
||||
const locale = localStorage.getItem('locale') || 'en-us';
|
||||
this.intl.setLocale(locale);
|
||||
|
||||
raven.config(config.SENTRY.DSN).install();
|
||||
|
||||
|
@ -2,9 +2,13 @@ import Component from '@glimmer/component';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
import {action} from '@ember/object';
|
||||
|
||||
export default class AccAvatarImg extends Component {
|
||||
interface Args {
|
||||
showFallback?: boolean;
|
||||
}
|
||||
|
||||
export default class AccAvatarImg extends Component<Args> {
|
||||
@tracked
|
||||
showFallback = false;
|
||||
showFallback = this.args.showFallback || false;
|
||||
|
||||
@action
|
||||
fallbackImage() {
|
||||
|
@ -17,7 +17,7 @@
|
||||
<div local-class="item-header">
|
||||
<div local-class="item-header-content">
|
||||
{{#if @activity.user.isBot}}
|
||||
<span local-class="item-user item-user--bot">
|
||||
<span local-class="item-user">
|
||||
{{@activity.user.fullname}}
|
||||
</span>
|
||||
{{else}}
|
||||
|
@ -1,7 +1,13 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import config from 'accent-webapp/config/environment';
|
||||
import {inject as service} from '@ember/service';
|
||||
import IntlService from 'ember-intl/services/intl';
|
||||
|
||||
export default class ApplicationFooter extends Component {
|
||||
@service('intl')
|
||||
intl: IntlService;
|
||||
|
||||
// The version is replaced at runtime when served by the API.
|
||||
// If the webapp is not served by the API (like in development),
|
||||
// the version tag will show up as 'dev'.
|
||||
@ -9,6 +15,10 @@ export default class ApplicationFooter extends Component {
|
||||
return config.version === '__VERSION__' ? 'dev' : config.version;
|
||||
}
|
||||
|
||||
get currentLocale() {
|
||||
return localStorage.getItem('locale') || 'en-us';
|
||||
}
|
||||
|
||||
toggleDark() {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
@ -18,4 +28,12 @@ export default class ApplicationFooter extends Component {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
localStorage.setItem('theme', 'light');
|
||||
}
|
||||
|
||||
@action
|
||||
changeLanguage(event: any) {
|
||||
const locale = event.target.value;
|
||||
|
||||
localStorage.setItem('locale', locale);
|
||||
this.intl.setLocale(locale);
|
||||
}
|
||||
}
|
||||
|
@ -14,11 +14,42 @@
|
||||
max-width: var(--screen-lg);
|
||||
}
|
||||
|
||||
.meta {
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.select {
|
||||
position: relative;
|
||||
font-size: 11px;
|
||||
|
||||
select {
|
||||
appearance: none;
|
||||
border: 1px solid var(--background-light-highlight);
|
||||
border-radius: 3px;
|
||||
padding: 2px 15px 2px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.select::after {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
cursor: pointer;
|
||||
content: '›';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 6px;
|
||||
font-size: 140%;
|
||||
transform: translateY(-50%) rotate(90deg);
|
||||
color: var(--text-color-normal);
|
||||
}
|
||||
|
||||
.version {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 10px;
|
||||
|
@ -1,6 +1,6 @@
|
||||
<footer local-class="footer">
|
||||
<div local-class="inner">
|
||||
<div local-class="meta">
|
||||
<div local-class="left">
|
||||
<span local-class="version">
|
||||
{{this.version}}
|
||||
</span>
|
||||
@ -15,11 +15,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{t "components.application_footer.text"}}
|
||||
<a local-class="external-link" target="_blank" rel="noopener" href="https://mirego.com?ref=accent">
|
||||
{{t "general.company_name"}}
|
||||
</a>
|
||||
<div local-class="right">
|
||||
<div>
|
||||
{{t "components.application_footer.text"}}
|
||||
<a local-class="external-link" target="_blank" rel="noopener" href="https://mirego.com?ref=accent">
|
||||
{{t "general.company_name"}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div local-class="select">
|
||||
<select {{on "change" this.changeLanguage}}>
|
||||
<option selected={{eq this.currentLocale 'en-us'}} value="en-us">En</option>
|
||||
<option selected={{eq this.currentLocale 'fr-ca'}} value="fr-ca">Fr</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -62,6 +62,7 @@
|
||||
{{#if this.file}}
|
||||
<div>
|
||||
{{#if @documents}}
|
||||
{{#if this.isSync}}
|
||||
<div local-class="option">
|
||||
<p local-class="textHelper">
|
||||
{{t "components.commit_file.file_source"}}
|
||||
@ -79,6 +80,7 @@
|
||||
</p>
|
||||
<Input @value={{this.documentPath}} local-class="fileSourceName" />
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<div local-class="option">
|
||||
|
@ -8,6 +8,7 @@
|
||||
margin-bottom: 20px;
|
||||
margin-right: 20px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--background-light-highlight);
|
||||
|
||||
&:nth-child(even) {
|
||||
flex: 1 1 50%;
|
||||
@ -59,7 +60,7 @@
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
padding: 8px 10px;
|
||||
color: var(--color-grey);
|
||||
}
|
||||
|
||||
@ -97,8 +98,8 @@
|
||||
transition: 0.2s ease-in-out;
|
||||
transition-property: opacity;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
right: 3px;
|
||||
top: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -124,19 +125,18 @@
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 15px;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.actionItem-text {
|
||||
margin-bottom: 7px;
|
||||
font-size: 12px;
|
||||
color: var(--color-grey);
|
||||
}
|
||||
|
||||
.actionItem-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 440px) {
|
||||
.language-reviewedPercentage {
|
||||
font-size: 18px;
|
||||
|
@ -147,7 +147,7 @@
|
||||
}
|
||||
|
||||
.master {
|
||||
margin: 0 0 20px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.stats-title-links {
|
||||
@ -220,8 +220,4 @@
|
||||
.dashboard-revisions > .content {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
.empty-content {
|
||||
padding: 15px 16px 16px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
max-width: 520px;
|
||||
color: hsl(var(--color-primary-hue), var(--color-primary-saturation), 30%);
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
|
@ -1,8 +1,107 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {action} from '@ember/object';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
import {dropTask} from 'ember-concurrency';
|
||||
import {taskFor} from 'ember-concurrency-ts';
|
||||
import IntlService from 'ember-intl/services/intl';
|
||||
import {CreateApiTokenResponse} from 'accent-webapp/queries/create-api-token';
|
||||
|
||||
interface Args {
|
||||
projectToken: string;
|
||||
userToken: string;
|
||||
onCreate: (args: {
|
||||
name: string;
|
||||
pictureUrl: string | null;
|
||||
permissions: string[];
|
||||
}) => void;
|
||||
onRevoke: (args: {id: string}) => void;
|
||||
}
|
||||
|
||||
export default class APIToken extends Component<Args> {}
|
||||
export default class APIToken extends Component<Args> {
|
||||
@service('intl')
|
||||
intl: IntlService;
|
||||
|
||||
@tracked
|
||||
isEdit = false;
|
||||
|
||||
@tracked
|
||||
showPermissionsInput = false;
|
||||
|
||||
@tracked
|
||||
apiTokenName = '';
|
||||
|
||||
@tracked
|
||||
apiTokenPermissions: string[] = [];
|
||||
|
||||
@tracked
|
||||
apiTokenPictureUrl: string | null = null;
|
||||
|
||||
@action
|
||||
onToggleForm() {
|
||||
this.isEdit = !this.isEdit;
|
||||
}
|
||||
|
||||
get isSubmitting() {
|
||||
return taskFor(this.submitTask).isRunning;
|
||||
}
|
||||
|
||||
get isSubmitDisabled() {
|
||||
return !this.apiTokenName || this.apiTokenName.length === 0;
|
||||
}
|
||||
|
||||
@dropTask
|
||||
*submitTask() {
|
||||
if (!this.apiTokenName) return;
|
||||
|
||||
const response: CreateApiTokenResponse = yield this.args.onCreate({
|
||||
name: this.apiTokenName,
|
||||
pictureUrl: this.apiTokenPictureUrl,
|
||||
permissions: this.apiTokenPermissions,
|
||||
});
|
||||
console.log(response);
|
||||
|
||||
if (response.apiToken) {
|
||||
this.apiTokenName = '';
|
||||
this.apiTokenPictureUrl = '';
|
||||
this.showPermissionsInput = false;
|
||||
this.unselectAllPermissions();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
changePermission() {
|
||||
this.apiTokenPermissions = Array.from(
|
||||
document.querySelectorAll('input[name="permiss"]:checked')
|
||||
).map((input: HTMLInputElement) => input.value);
|
||||
}
|
||||
|
||||
@action
|
||||
togglePermissionsInput() {
|
||||
this.showPermissionsInput = !this.showPermissionsInput;
|
||||
}
|
||||
|
||||
@action
|
||||
selectAllPermissions() {
|
||||
Array.from(document.querySelectorAll('input[name="permiss"]')).forEach(
|
||||
(input: HTMLInputElement) => (input.checked = true)
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
unselectAllPermissions() {
|
||||
Array.from(document.querySelectorAll('input[name="permiss"]')).forEach(
|
||||
(input: HTMLInputElement) => (input.checked = false)
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
apiTokenNameChanged(event: any) {
|
||||
this.apiTokenName = event.target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
apiTokenPictureUrlChanged(event: any) {
|
||||
this.apiTokenPictureUrl = event.target.value;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {action} from '@ember/object';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
import {dropTask} from 'ember-concurrency';
|
||||
import {taskFor} from 'ember-concurrency-ts';
|
||||
import IntlService from 'ember-intl/services/intl';
|
||||
|
||||
interface Args {
|
||||
token: any;
|
||||
onRevoke: (args: {id: string}) => void;
|
||||
}
|
||||
|
||||
export default class APITokenItem extends Component<Args> {
|
||||
@service('intl')
|
||||
intl: IntlService;
|
||||
|
||||
@tracked
|
||||
showPermissions = false;
|
||||
|
||||
get isRevoking() {
|
||||
return taskFor(this.revokeTask).isRunning;
|
||||
}
|
||||
|
||||
@dropTask
|
||||
*revokeTask() {
|
||||
const message = this.intl.t(
|
||||
'components.project_settings.api_token.revoke_confirm'
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-alert
|
||||
if (!window.confirm(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield this.args.onRevoke(this.args.token);
|
||||
}
|
||||
|
||||
@action
|
||||
togglePermissions() {
|
||||
this.showPermissions = !this.showPermissions;
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
.api-token {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.api-token:hover .revoke-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.api-token-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.api-token-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.api-token-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.api-token-name {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.api-token-inserted {
|
||||
opacity: 0.3;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.api-token-permissions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.api-token-permission {
|
||||
font-size: 11px;
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
.revoke-button {
|
||||
opacity: 0;
|
||||
transition: 0.2s ease-in-out;
|
||||
transition-property: opacity;
|
||||
padding: 2px 4px !important;
|
||||
}
|
||||
|
||||
.token {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 2px 8px;
|
||||
overflow-x: scroll;
|
||||
word-break: keep-all;
|
||||
background: var(--background-light);
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 11px;
|
||||
border-radius: 8px;
|
||||
transition-property: border-color, box-shadow;
|
||||
transition: 0.3s ease-in-out;
|
||||
color: var(--text-color-normal);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary-opacity-70);
|
||||
box-shadow: 0 0 3px 2px var(--color-primary-opacity-10);
|
||||
|
||||
&::-moz-selection {
|
||||
background: var(--color-primary-opacity-10);
|
||||
}
|
||||
|
||||
&::selection {
|
||||
background: var(--color-primary-opacity-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-permissions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: transparent;
|
||||
padding: 3px 0;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.toggle-permissions-icon {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
transition: 0.2s ease-in-out;
|
||||
transition-property: transform;
|
||||
}
|
||||
|
||||
.toggle-permissions--open {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toggle-permissions--open .toggle-permissions-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
<div local-class="api-token">
|
||||
<div local-class="api-token-header">
|
||||
<div local-class="api-token-meta">
|
||||
<div local-class="api-token-user">
|
||||
{{#if @token.user.pictureUrl}}
|
||||
<AccAvatarImg local-class="picture" src={{@token.user.pictureUrl}} />
|
||||
{{else}}
|
||||
<AccAvatarImg local-class="picture" src={{@token.user.pictureUrl}} @showFallback={{true}} />
|
||||
{{/if}}
|
||||
|
||||
<span local-class="api-token-name">{{@token.user.fullname}}</span>
|
||||
</div>
|
||||
<span local-class="api-token-inserted">
|
||||
{{t "components.project_settings.api_token.inserted_at"}}
|
||||
<TimeAgoInWordsTag @date={{@token.insertedAt}} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{#if (get @permissions "revoke_project_api_token")}}
|
||||
<AsyncButton
|
||||
@onClick={{perform this.revokeTask}}
|
||||
@loading={{this.isRevoking}}
|
||||
@disabled={{this.isRevoking}}
|
||||
local-class="revoke-button"
|
||||
class="button button--small button--filled button--red"
|
||||
>
|
||||
{{inline-svg "/assets/x.svg" class="button-icon"}}
|
||||
{{t "components.project_settings.api_token.revoke_button"}}
|
||||
</AsyncButton>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input readonly="" onClick="this.select();" local-class="token" value={{@token.token}}>
|
||||
{{#if @token.customPermissions}}
|
||||
<button
|
||||
{{on "click" (fn this.togglePermissions)}}
|
||||
local-class="toggle-permissions {{if this.showPermissions 'toggle-permissions--open'}}"
|
||||
>Custom permissions
|
||||
<span local-class="toggle-permissions-icon">↓</span>
|
||||
</button>
|
||||
|
||||
{{#if this.showPermissions}}
|
||||
<ul local-class="api-token-permissions">
|
||||
{{#each @token.customPermissions as |permission|}}
|
||||
<li local-class="api-token-permission">{{permission}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
@ -1,31 +1,225 @@
|
||||
.project-settings-api-token {
|
||||
display: flex;
|
||||
gap: 35px;
|
||||
align-items: flex-start;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.text {
|
||||
max-width: 490px;
|
||||
margin: 10px 0 4px;
|
||||
margin: 10px 0 15px;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.text code {
|
||||
font-family: var(--font-monospace);
|
||||
padding: 1px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.tokens {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user-token {
|
||||
margin-top: 30px;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
border-radius: 3px;
|
||||
background-color: var(--background-light);
|
||||
}
|
||||
|
||||
.user-token .token {
|
||||
background-color: var(--background-light-highlight);
|
||||
}
|
||||
|
||||
.user-token h2 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.api-tokens {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 22px;
|
||||
}
|
||||
|
||||
.api-tokens.overlay {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
background: rgba(#fff, 0.5);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.api-token {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.api-token:hover .revoke-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.api-token-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.api-token-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.api-token-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.api-token-name {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.api-token-inserted {
|
||||
opacity: 0.3;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.api-token-permissions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.api-token-permissions-label {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.api-token-permission {
|
||||
padding: 1px 3px;
|
||||
border-radius: 3px;
|
||||
background-color: var(--background-light);
|
||||
border: 1px solid var(--background-light-highlight);
|
||||
font-size: 11px;
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
.revoke-button {
|
||||
opacity: 0;
|
||||
transition: 0.2s ease-in-out;
|
||||
transition-property: opacity;
|
||||
padding: 2px 4px !important;
|
||||
}
|
||||
|
||||
.create-button {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.permissions-inputs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.permissions-input {
|
||||
font-size: 12px;
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
.toggle-permissions-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toggle-permissions-header-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.toggle-permissions-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: transparent;
|
||||
border-bottom: 1px solid var(--text-color-normal);
|
||||
padding: 3px 0;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.toggle-permissions-input-icon {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
transition: 0.2s ease-in-out;
|
||||
transition-property: transform;
|
||||
}
|
||||
|
||||
.toggle-permissions-input--open {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toggle-permissions-input--open .toggle-permissions-input-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.picture {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
object-fit: cover;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-top: 36px;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
background: var(--background-light);
|
||||
border: 1px solid var(--body-background-border);
|
||||
}
|
||||
|
||||
.form h2 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.textInput {
|
||||
@extend %textInput;
|
||||
padding: 4px 10px;
|
||||
margin-right: 5px;
|
||||
width: 100%;
|
||||
font-family: var(--font-primary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.token {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
margin: 10px 0 0;
|
||||
padding: 8px;
|
||||
margin: 0;
|
||||
padding: 2px 8px;
|
||||
overflow-x: scroll;
|
||||
word-break: keep-all;
|
||||
background: var(--background-light);
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 11px;
|
||||
border: 2px solid var(--background-light-highlight);
|
||||
border-radius: 3px;
|
||||
border-radius: 8px;
|
||||
transition-property: border-color, box-shadow;
|
||||
transition: 0.3s ease-in-out;
|
||||
color: var(--text-color-normal);
|
||||
@ -44,3 +238,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 840px) {
|
||||
.project-settings-api-token {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,100 @@
|
||||
<div local-class="project-settings-api-token">
|
||||
{{#if (get @permissions "list_project_api_tokens")}}
|
||||
<div local-class="tokens">
|
||||
<ProjectSettings::Title
|
||||
@icon="/assets/code.svg"
|
||||
@title={{t "components.project_settings.api_token.title"}}
|
||||
/>
|
||||
|
||||
<p local-class="text">
|
||||
{{t "components.project_settings.api_token.text_1"}}
|
||||
{{t "components.project_settings.api_token.text_2"}}
|
||||
{{{t "components.project_settings.api_token.text_1"}}}
|
||||
</p>
|
||||
|
||||
<input readonly="" onClick="this.select();" local-class="token" value={{@projectToken}}>
|
||||
<div local-class="api-tokens {{if this.isRevoking 'overlay'}}">
|
||||
{{#each @projectTokens key="id" as |token|}}
|
||||
<ProjectSettings::ApiToken::Item @token={{token}} @permissions={{@permissions}} @onRevoke={{@onRevoke}}/>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
{{#if (get @permissions "create_project_api_token")}}
|
||||
<div local-class="form">
|
||||
<ProjectSettings::Title
|
||||
@title={{t "components.project_settings.api_token.create_title"}}
|
||||
/>
|
||||
|
||||
<input
|
||||
{{on-key "cmd+Enter" (perform this.submitTask)}}
|
||||
{{on "input" (fn this.apiTokenNameChanged)}}
|
||||
local-class="textInput"
|
||||
type="text"
|
||||
value={{this.apiTokenName}}
|
||||
placeholder={{t "components.project_settings.api_token.create_name_placeholder"}}
|
||||
>
|
||||
|
||||
<input
|
||||
{{on-key "cmd+Enter" (perform this.submitTask)}}
|
||||
{{on "input" (fn this.apiTokenPictureUrlChanged)}}
|
||||
local-class="textInput"
|
||||
type="url"
|
||||
value={{this.apiTokenPictureUrl}}
|
||||
placeholder={{t "components.project_settings.api_token.create_picture_url_placeholder"}}
|
||||
>
|
||||
<div local-class="toggle-permissions-header">
|
||||
|
||||
<button
|
||||
{{on "click" (fn this.togglePermissionsInput)}}
|
||||
local-class="toggle-permissions-input {{if this.showPermissionsInput 'toggle-permissions-input--open'}}"
|
||||
>Use custom permissions
|
||||
<span local-class="toggle-permissions-input-icon">↓</span>
|
||||
</button>
|
||||
|
||||
{{#if this.showPermissionsInput}}
|
||||
<div>
|
||||
<button
|
||||
{{on "click" (fn this.selectAllPermissions)}}
|
||||
local-class="toggle-permissions-header-button"
|
||||
>Select all
|
||||
</button>
|
||||
<button
|
||||
{{on "click" (fn this.unselectAllPermissions)}}
|
||||
local-class="toggle-permissions-header-button"
|
||||
>Unselect all
|
||||
</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if this.showPermissionsInput}}
|
||||
<ul local-class="permissions-inputs">
|
||||
{{#each-in @permissions as |permission|}}
|
||||
<li local-class="permissions-input">
|
||||
<label for={{concat "permission-" permission}}>
|
||||
|
||||
<input name="permiss" value={{permission}} type="checkbox"
|
||||
id={{concat "permission-" permission}}
|
||||
{{on "input" (fn this.changePermission)}}
|
||||
/>
|
||||
{{permission}}
|
||||
</label>
|
||||
</li>
|
||||
{{/each-in}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
|
||||
<AsyncButton
|
||||
local-class="create-button"
|
||||
class="button button--filled"
|
||||
@disabled={{this.isSubmitDisabled}}
|
||||
@onClick={{perform this.submitTask}}
|
||||
@loading={{this.isSubmitting}}
|
||||
>
|
||||
{{t "components.project_settings.api_token.create_button"}}
|
||||
</AsyncButton>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div local-class="user-token">
|
||||
<ProjectSettings::Title
|
||||
@ -21,6 +106,10 @@
|
||||
{{t "components.project_settings.user_token.text_1"}}
|
||||
</p>
|
||||
|
||||
<p local-class="text">
|
||||
{{{t "components.project_settings.user_token.text_shell"}}}
|
||||
</p>
|
||||
|
||||
<input readonly="" onClick="this.select();" local-class="token" value={{@userToken}}>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -22,7 +22,7 @@
|
||||
.badge-title {
|
||||
margin: 0 10px 0 0;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
font-weight: bold;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,14 @@
|
||||
<div local-class="project-settings-collaborators">
|
||||
<div local-class="columns">
|
||||
<div local-class="columns-item">
|
||||
{{#if (get @permissions "create_collaborator")}}
|
||||
<div local-class="createForm">
|
||||
<ProjectSettings::Collaborators::CreateForm
|
||||
@project={{@project}}
|
||||
@onCreate={{@onCreateCollaborator}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<ProjectSettings::Collaborators::List
|
||||
@permissions={{@permissions}}
|
||||
|
@ -18,14 +18,13 @@ interface Args {
|
||||
|
||||
export default class GitHub extends Component<Args> {
|
||||
get webhookUrl() {
|
||||
if (!this.args.project.accessToken) return;
|
||||
const host = window.location.origin;
|
||||
|
||||
return `${host}${fmt(
|
||||
config.API.HOOKS_PATH,
|
||||
'github',
|
||||
this.args.project.id,
|
||||
this.args.project.accessToken
|
||||
'<YOUR_API_TOKEN_HERE>'
|
||||
)}`;
|
||||
}
|
||||
|
||||
|
@ -48,6 +48,7 @@ export default class ProjectsHeader extends Component<Args> {
|
||||
@restartableTask
|
||||
*debounceQuery(query: string) {
|
||||
this.debouncedQuery = query;
|
||||
if (!this.debouncedQuery) return;
|
||||
|
||||
yield timeout(DEBOUNCE_OFFSET);
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
.projects-header {
|
||||
padding: 64px 0 20px;
|
||||
padding: 40px 0 20px;
|
||||
background-image: linear-gradient(
|
||||
0,
|
||||
var(--body-background),
|
||||
@ -8,8 +8,6 @@
|
||||
}
|
||||
|
||||
.projects-header.withProject {
|
||||
padding-top: 59px;
|
||||
|
||||
.applicationLogo {
|
||||
position: absolute;
|
||||
padding-right: 10px;
|
||||
@ -143,7 +141,7 @@
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
top: 6px;
|
||||
left: 10px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
|
@ -1,16 +1,16 @@
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
height: 5px;
|
||||
background: var(--background-light);
|
||||
border-radius: 3px;
|
||||
border-radius: 1px;
|
||||
box-shadow: 0 1px 8px var(--shadow-color);
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 2px;
|
||||
height: 5px;
|
||||
width: 0;
|
||||
background: currentColor;
|
||||
border-radius: 3px;
|
||||
border-radius: 1px;
|
||||
transition: width 500ms ease-in-out;
|
||||
|
||||
&:after {
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
margin-top: 10px;
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
margin-top: 10px;
|
||||
|
@ -1,28 +1,112 @@
|
||||
import {inject as service} from '@ember/service';
|
||||
import {action} from '@ember/object';
|
||||
import {readOnly, equal, and} from '@ember/object/computed';
|
||||
import Controller from '@ember/controller';
|
||||
import IntlService from 'ember-intl/services/intl';
|
||||
import GlobalState from 'accent-webapp/services/global-state';
|
||||
import FlashMessages from 'ember-cli-flash/services/flash-messages';
|
||||
import ApolloMutate from 'accent-webapp/services/apollo-mutate';
|
||||
|
||||
import apiTokenCreateQuery from 'accent-webapp/queries/create-api-token';
|
||||
import apiTokenRevokeQuery from 'accent-webapp/queries/revoke-api-token';
|
||||
|
||||
const FLASH_MESSAGE_PREFIX = 'pods.project.edit.flash_messages.';
|
||||
const FLASH_MESSAGE_API_TOKEN_ADD_SUCCESS = `${FLASH_MESSAGE_PREFIX}api_token_add_success`;
|
||||
const FLASH_MESSAGE_API_TOKEN_ADD_ERROR = `${FLASH_MESSAGE_PREFIX}api_token_add_error`;
|
||||
const FLASH_MESSAGE_API_TOKEN_REVOKE_SUCCESS = `${FLASH_MESSAGE_PREFIX}api_token_revoke_success`;
|
||||
const FLASH_MESSAGE_API_TOKEN_REVOKE_ERROR = `${FLASH_MESSAGE_PREFIX}api_token_revoke_error`;
|
||||
|
||||
export default class APITokenController extends Controller {
|
||||
@service('intl')
|
||||
intl: IntlService;
|
||||
|
||||
@service('flash-messages')
|
||||
flashMessages: FlashMessages;
|
||||
|
||||
@service('global-state')
|
||||
globalState: GlobalState;
|
||||
|
||||
@service('apollo-mutate')
|
||||
apolloMutate: ApolloMutate;
|
||||
|
||||
@readOnly('globalState.permissions')
|
||||
permissions: any;
|
||||
|
||||
@readOnly('model.project')
|
||||
project: any;
|
||||
|
||||
@readOnly('model.accessToken')
|
||||
accessToken: string;
|
||||
|
||||
@readOnly('globalState.permissions')
|
||||
permissions: any;
|
||||
@readOnly('model.apiTokens')
|
||||
apiTokens: any;
|
||||
|
||||
@equal('model.project.name', undefined)
|
||||
emptyData: boolean;
|
||||
|
||||
@and('emptyData', 'model.loading')
|
||||
showLoading: boolean;
|
||||
|
||||
@action
|
||||
async createApiToken({
|
||||
name,
|
||||
pictureUrl,
|
||||
permissions,
|
||||
}: {
|
||||
name: string;
|
||||
pictureUrl: string | null;
|
||||
permissions: string[];
|
||||
}) {
|
||||
const project = this.project;
|
||||
|
||||
return this.mutateResource({
|
||||
mutation: apiTokenCreateQuery,
|
||||
successMessage: FLASH_MESSAGE_API_TOKEN_ADD_SUCCESS,
|
||||
errorMessage: FLASH_MESSAGE_API_TOKEN_ADD_ERROR,
|
||||
variables: {
|
||||
projectId: project.id,
|
||||
pictureUrl,
|
||||
name,
|
||||
permissions,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async revokeApiToken(apiToken: {id: string}) {
|
||||
return this.mutateResource({
|
||||
mutation: apiTokenRevokeQuery,
|
||||
successMessage: FLASH_MESSAGE_API_TOKEN_REVOKE_SUCCESS,
|
||||
errorMessage: FLASH_MESSAGE_API_TOKEN_REVOKE_ERROR,
|
||||
variables: {
|
||||
id: apiToken.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async mutateResource({
|
||||
mutation,
|
||||
variables,
|
||||
successMessage,
|
||||
errorMessage,
|
||||
}: {
|
||||
mutation: any;
|
||||
variables: any;
|
||||
successMessage: string;
|
||||
errorMessage: string;
|
||||
}) {
|
||||
const response = await this.apolloMutate.mutate({
|
||||
mutation,
|
||||
variables,
|
||||
refetchQueries: ['ProjectApiToken'],
|
||||
});
|
||||
|
||||
if (response.errors) {
|
||||
this.flashMessages.error(this.intl.t(errorMessage));
|
||||
} else {
|
||||
this.flashMessages.success(this.intl.t(successMessage));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
@ -23,8 +23,9 @@ export default class APITokenRoute extends Route {
|
||||
projectApiTokenQuery,
|
||||
{
|
||||
props: (data) => ({
|
||||
accessToken: data.viewer.accessToken,
|
||||
project: data.viewer.project,
|
||||
accessToken: data.viewer.accessToken,
|
||||
apiToken: data.viewer.project.apiTokens,
|
||||
}),
|
||||
options: {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
|
@ -3,7 +3,11 @@
|
||||
{{else}}
|
||||
<ProjectSettings::BackLink @project={{this.project}} />
|
||||
|
||||
{{#if this.project.accessToken}}
|
||||
<ProjectSettings::ApiToken @projectToken={{this.project.accessToken}} @userToken={{this.accessToken}} />
|
||||
{{/if}}
|
||||
<ProjectSettings::ApiToken
|
||||
@projectTokens={{this.project.apiTokens}}
|
||||
@permissions={{this.permissions}}
|
||||
@userToken={{this.accessToken}}
|
||||
@onCreate={{fn this.createApiToken}}
|
||||
@onRevoke={{fn this.revokeApiToken}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
38
webapp/app/queries/create-api-token.ts
Normal file
38
webapp/app/queries/create-api-token.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export interface CreateApiTokenVariables {
|
||||
name: string;
|
||||
projectId: string;
|
||||
pictureUrl: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export interface CreateApiTokenResponse {
|
||||
apiToken: {
|
||||
id: string;
|
||||
};
|
||||
|
||||
errors: any;
|
||||
}
|
||||
|
||||
export default gql`
|
||||
mutation ApiTokenCreate(
|
||||
$name: String!
|
||||
$pictureUrl: String
|
||||
$projectId: ID!
|
||||
$permissions: [String!]
|
||||
) {
|
||||
createApiToken(
|
||||
name: $name
|
||||
pictureUrl: $pictureUrl
|
||||
permissions: $permissions
|
||||
projectId: $projectId
|
||||
) {
|
||||
apiToken {
|
||||
id
|
||||
}
|
||||
|
||||
errors
|
||||
}
|
||||
}
|
||||
`;
|
@ -8,7 +8,18 @@ export default gql`
|
||||
project(id: $projectId) {
|
||||
id
|
||||
name
|
||||
accessToken
|
||||
|
||||
apiTokens {
|
||||
id
|
||||
token
|
||||
insertedAt
|
||||
customPermissions
|
||||
user {
|
||||
id
|
||||
fullname
|
||||
pictureUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ export default gql`
|
||||
project(id: $projectId) {
|
||||
id
|
||||
name
|
||||
accessToken
|
||||
|
||||
integrations {
|
||||
id
|
||||
|
11
webapp/app/queries/revoke-api-token.ts
Normal file
11
webapp/app/queries/revoke-api-token.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export interface RevokeApiTokenVariables {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export default gql`
|
||||
mutation ApiTokenRevoke($id: ID!) {
|
||||
revokeApiToken(id: $id)
|
||||
}
|
||||
`;
|
@ -3,7 +3,7 @@ import gql from 'graphql-tag';
|
||||
export default gql`
|
||||
query TranslateTextProject(
|
||||
$projectId: ID!
|
||||
$text: String
|
||||
$text: String!
|
||||
$sourceLanguageSlug: String!
|
||||
$targetLanguageSlug: String!
|
||||
) {
|
||||
|
@ -119,6 +119,7 @@ input[type="radio"] {
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
flex-shrink: 0;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
@ -138,4 +139,4 @@ select,
|
||||
textarea {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,6 @@ export const fakeProject = (params?: object) => ({
|
||||
'sync',
|
||||
'delete_document',
|
||||
'update_document',
|
||||
'show_project_access_token',
|
||||
'index_project_integrations',
|
||||
'create_project_integration',
|
||||
'update_project_integration',
|
||||
|
Loading…
Reference in New Issue
Block a user