mirror of
https://github.com/google/nomulus.git
synced 2025-05-14 16:37:13 +02:00
Add the ability to setup OT&E from the web console
We create a new endpoint with a simple form that will let admins (including support) setup OT&E for registrars. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=226570568
This commit is contained in:
parent
040319a95d
commit
2777018d6a
17 changed files with 804 additions and 34 deletions
43
java/google/registry/ui/server/otesetup/BUILD
Normal file
43
java/google/registry/ui/server/otesetup/BUILD
Normal file
|
@ -0,0 +1,43 @@
|
|||
package(
|
||||
default_visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
licenses(["notice"]) # Apache 2.0
|
||||
|
||||
java_library(
|
||||
name = "otesetup",
|
||||
srcs = glob(["*.java"]),
|
||||
resources = [
|
||||
"//java/google/registry/ui/css:registrar_bin.css.js",
|
||||
"//java/google/registry/ui/css:registrar_dbg.css.js",
|
||||
],
|
||||
deps = [
|
||||
"//java/google/registry/config",
|
||||
"//java/google/registry/export/sheet",
|
||||
"//java/google/registry/flows",
|
||||
"//java/google/registry/model",
|
||||
"//java/google/registry/request",
|
||||
"//java/google/registry/request/auth",
|
||||
"//java/google/registry/security",
|
||||
"//java/google/registry/ui/forms",
|
||||
"//java/google/registry/ui/server",
|
||||
"//java/google/registry/ui/soy:soy_java_wrappers",
|
||||
"//java/google/registry/ui/soy/otesetup:soy_java_wrappers",
|
||||
"//java/google/registry/util",
|
||||
"//third_party/objectify:objectify-v4_1",
|
||||
"@com_google_appengine_api_1_0_sdk",
|
||||
"@com_google_auto_value",
|
||||
"@com_google_code_findbugs_jsr305",
|
||||
"@com_google_dagger",
|
||||
"@com_google_flogger",
|
||||
"@com_google_flogger_system_backend",
|
||||
"@com_google_guava",
|
||||
"@com_google_monitoring_client_metrics",
|
||||
"@com_google_re2j",
|
||||
"@io_bazel_rules_closure//closure/templates",
|
||||
"@javax_inject",
|
||||
"@javax_servlet_api",
|
||||
"@joda_time",
|
||||
"@org_joda_money",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,243 @@
|
|||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.ui.server.otesetup;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.net.HttpHeaders.LOCATION;
|
||||
import static com.google.common.net.HttpHeaders.X_FRAME_OPTIONS;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY;
|
||||
|
||||
import com.google.appengine.api.users.User;
|
||||
import com.google.appengine.api.users.UserService;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.common.net.MediaType;
|
||||
import com.google.template.soy.shared.SoyCssRenamingMap;
|
||||
import com.google.template.soy.tofu.SoyTofu;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.OteAccountBuilder;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Action.Method;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.RequestMethod;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.request.auth.AuthResult;
|
||||
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
|
||||
import google.registry.security.XsrfTokenManager;
|
||||
import google.registry.ui.server.SendEmailUtils;
|
||||
import google.registry.ui.server.SoyTemplateUtils;
|
||||
import google.registry.ui.soy.otesetup.ConsoleSoyInfo;
|
||||
import google.registry.util.StringGenerator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* Action that serves OT&E setup web page.
|
||||
*
|
||||
* <p>This Action does 2 things: - for GET, just returns the form that asks for the clientId and
|
||||
* email. - for POST, receives the clientId and email and generates the OTE entities.
|
||||
*
|
||||
* <p>TODO(b/120201577): once we can have 2 different Actions with the same path (different
|
||||
* Methods), separate this class to 2 Actions.
|
||||
*/
|
||||
@Action(
|
||||
path = ConsoleOteSetupAction.PATH,
|
||||
method = {Method.POST, Method.GET},
|
||||
auth = Auth.AUTH_PUBLIC)
|
||||
public final class ConsoleOteSetupAction implements Runnable {
|
||||
|
||||
private static final int PASSWORD_LENGTH = 16;
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
public static final String PATH = "/registrar-ote-setup";
|
||||
|
||||
private static final Supplier<SoyTofu> TOFU_SUPPLIER =
|
||||
SoyTemplateUtils.createTofuSupplier(
|
||||
google.registry.ui.soy.ConsoleSoyInfo.getInstance(),
|
||||
google.registry.ui.soy.FormsSoyInfo.getInstance(),
|
||||
google.registry.ui.soy.otesetup.ConsoleSoyInfo.getInstance());
|
||||
|
||||
@VisibleForTesting // webdriver and screenshot tests need this
|
||||
public static final Supplier<SoyCssRenamingMap> CSS_RENAMING_MAP_SUPPLIER =
|
||||
SoyTemplateUtils.createCssRenamingMapSupplier(
|
||||
Resources.getResource("google/registry/ui/css/registrar_bin.css.js"),
|
||||
Resources.getResource("google/registry/ui/css/registrar_dbg.css.js"));
|
||||
|
||||
@Inject HttpServletRequest req;
|
||||
@Inject @RequestMethod Method method;
|
||||
@Inject Response response;
|
||||
@Inject AuthenticatedRegistrarAccessor registrarAccessor;
|
||||
@Inject UserService userService;
|
||||
@Inject XsrfTokenManager xsrfTokenManager;
|
||||
@Inject AuthResult authResult;
|
||||
@Inject RegistryEnvironment registryEnvironment;
|
||||
@Inject SendEmailUtils sendEmailUtils;
|
||||
@Inject @Config("logoFilename") String logoFilename;
|
||||
@Inject @Config("productName") String productName;
|
||||
@Inject @Config("base64StringGenerator") StringGenerator passwordGenerator;
|
||||
@Inject @Parameter("clientId") Optional<String> clientId;
|
||||
@Inject @Parameter("email") Optional<String> email;
|
||||
|
||||
@Inject ConsoleOteSetupAction() {}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
response.setHeader(X_FRAME_OPTIONS, "SAMEORIGIN"); // Disallow iframing.
|
||||
response.setHeader("X-Ui-Compatible", "IE=edge"); // Ask IE not to be silly.
|
||||
|
||||
logger.atInfo().log(
|
||||
"User %s is accessing the OT&E setup page. Method= %s",
|
||||
registrarAccessor.userIdForLogging(), method);
|
||||
checkState(registryEnvironment != RegistryEnvironment.PRODUCTION, "Can't create OT&E in prod");
|
||||
if (!authResult.userAuthInfo().isPresent()) {
|
||||
response.setStatus(SC_MOVED_TEMPORARILY);
|
||||
String location;
|
||||
try {
|
||||
location = userService.createLoginURL(req.getRequestURI());
|
||||
} catch (IllegalArgumentException e) {
|
||||
// UserServiceImpl.createLoginURL() throws IllegalArgumentException if underlying API call
|
||||
// returns an error code of NOT_ALLOWED. createLoginURL() assumes that the error is caused
|
||||
// by an invalid URL. But in fact, the error can also occur if UserService doesn't have any
|
||||
// user information, which happens when the request has been authenticated as internal. In
|
||||
// this case, we want to avoid dying before we can send the redirect, so just redirect to
|
||||
// the root path.
|
||||
location = "/";
|
||||
}
|
||||
response.setHeader(LOCATION, location);
|
||||
return;
|
||||
}
|
||||
User user = authResult.userAuthInfo().get().user();
|
||||
|
||||
// Using HashMap to allow null values
|
||||
HashMap<String, Object> data = new HashMap<>();
|
||||
data.put("logoFilename", logoFilename);
|
||||
data.put("productName", productName);
|
||||
data.put("username", user.getNickname());
|
||||
data.put("logoutUrl", userService.createLogoutURL(PATH));
|
||||
data.put("xsrfToken", xsrfTokenManager.generateToken(user.getEmail()));
|
||||
response.setContentType(MediaType.HTML_UTF_8);
|
||||
|
||||
if (!registrarAccessor.isAdmin()) {
|
||||
response.setStatus(SC_FORBIDDEN);
|
||||
response.setPayload(
|
||||
TOFU_SUPPLIER
|
||||
.get()
|
||||
.newRenderer(ConsoleSoyInfo.WHOAREYOU)
|
||||
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
|
||||
.setData(data)
|
||||
.render());
|
||||
return;
|
||||
}
|
||||
switch (method) {
|
||||
case POST:
|
||||
runPost(data);
|
||||
return;
|
||||
case GET:
|
||||
runGet(data);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void runPost(HashMap<String, Object> data) {
|
||||
// This is intentionally outside of the "try/catch", since not having these fields means someone
|
||||
// tried to "manually" POST to this page. No need to send out a "pretty" result in that case.
|
||||
checkState(clientId.isPresent() && email.isPresent(), "Must supply clientId and email");
|
||||
|
||||
data.put("baseClientId", clientId.get());
|
||||
data.put("contactEmail", email.get());
|
||||
|
||||
try {
|
||||
String password = passwordGenerator.createString(PASSWORD_LENGTH);
|
||||
ImmutableMap<String, String> clientIdToTld =
|
||||
OteAccountBuilder.forClientId(clientId.get())
|
||||
.addContact(email.get())
|
||||
.setPassword(password)
|
||||
.buildAndPersist();
|
||||
|
||||
sendExternalUpdates(clientIdToTld);
|
||||
|
||||
data.put("clientIdToTld", clientIdToTld);
|
||||
data.put("password", password);
|
||||
|
||||
response.setPayload(
|
||||
TOFU_SUPPLIER
|
||||
.get()
|
||||
.newRenderer(ConsoleSoyInfo.RESULT_SUCCESS)
|
||||
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
|
||||
.setData(data)
|
||||
.render());
|
||||
} catch (Throwable e) {
|
||||
logger.atWarning().withCause(e).log(
|
||||
"Failed to setup OT&E. clientId: %s, email: %s", clientId.get(), email.get());
|
||||
data.put("errorMessage", e.getMessage());
|
||||
response.setPayload(
|
||||
TOFU_SUPPLIER
|
||||
.get()
|
||||
.newRenderer(ConsoleSoyInfo.FORM_PAGE)
|
||||
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
|
||||
.setData(data)
|
||||
.render());
|
||||
}
|
||||
}
|
||||
|
||||
private void runGet(HashMap<String, Object> data) {
|
||||
// set the values to pre-fill, if given
|
||||
data.put("baseClientId", clientId.orElse(null));
|
||||
data.put("contactEmail", email.orElse(null));
|
||||
|
||||
response.setPayload(
|
||||
TOFU_SUPPLIER
|
||||
.get()
|
||||
.newRenderer(ConsoleSoyInfo.FORM_PAGE)
|
||||
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
|
||||
.setData(data)
|
||||
.render());
|
||||
}
|
||||
|
||||
|
||||
private void sendExternalUpdates(ImmutableMap<String, String> clientIdToTld) {
|
||||
if (!sendEmailUtils.hasRecepients()) {
|
||||
return;
|
||||
}
|
||||
String environment = Ascii.toLowerCase(String.valueOf(registryEnvironment));
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(
|
||||
String.format(
|
||||
"The following entities were created in %s by %s:\n",
|
||||
environment, registrarAccessor.userIdForLogging()));
|
||||
clientIdToTld.forEach(
|
||||
(clientId, tld) ->
|
||||
builder.append(
|
||||
String.format(" Registrar %s with access to TLD %s\n", clientId, tld)));
|
||||
builder.append(String.format("Gave user %s web access to these Registrars\n", email.get()));
|
||||
sendEmailUtils.sendEmail(
|
||||
String.format(
|
||||
"OT&E for registrar %s created in %s",
|
||||
clientId.get(),
|
||||
environment),
|
||||
builder.toString());
|
||||
}
|
||||
}
|
|
@ -41,4 +41,16 @@ public final class RegistrarConsoleModule {
|
|||
static String provideClientId(HttpServletRequest req) {
|
||||
return extractRequiredParameter(req, PARAM_CLIENT_ID);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("email")
|
||||
static Optional<String> provideOptionalEmail(HttpServletRequest req) {
|
||||
return extractOptionalParameter(req, "email");
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("email")
|
||||
static String provideEmail(HttpServletRequest req) {
|
||||
return extractRequiredParameter(req, "email");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -295,6 +295,21 @@
|
|||
</tr>
|
||||
{/template}
|
||||
|
||||
/** Submit button. */
|
||||
{template .submitRow}
|
||||
{@param label: string}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<input
|
||||
id="submit-button"
|
||||
type="submit"
|
||||
value="{$label}"
|
||||
class="{css('kd-button')} {css('kd-button-submit')}">
|
||||
</td>
|
||||
</tr>
|
||||
{/template}
|
||||
|
||||
|
||||
/** Drop-down select button widget. */
|
||||
{template .menuButton}
|
||||
|
|
21
java/google/registry/ui/soy/otesetup/BUILD
Normal file
21
java/google/registry/ui/soy/otesetup/BUILD
Normal file
|
@ -0,0 +1,21 @@
|
|||
package(
|
||||
default_visibility = ["//java/google/registry:registry_project"],
|
||||
)
|
||||
|
||||
licenses(["notice"]) # Apache 2.0
|
||||
|
||||
load("@io_bazel_rules_closure//closure:defs.bzl", "closure_java_template_library", "closure_js_template_library")
|
||||
|
||||
closure_js_template_library(
|
||||
name = "otesetup",
|
||||
srcs = glob(["*.soy"]),
|
||||
data = ["//java/google/registry/ui/css:registrar_raw"],
|
||||
globals = "//java/google/registry/ui:globals.txt",
|
||||
deps = ["//java/google/registry/ui/soy"],
|
||||
)
|
||||
|
||||
closure_java_template_library(
|
||||
name = "soy_java_wrappers",
|
||||
srcs = glob(["*.soy"]),
|
||||
java_package = "google.registry.ui.soy.otesetup",
|
||||
)
|
169
java/google/registry/ui/soy/otesetup/Console.soy
Normal file
169
java/google/registry/ui/soy/otesetup/Console.soy
Normal file
|
@ -0,0 +1,169 @@
|
|||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
{namespace registry.soy.registrar.console}
|
||||
|
||||
|
||||
/**
|
||||
* Main page for the OT&E creation. Holds a form with the required data.
|
||||
*/
|
||||
{template .formPage}
|
||||
{@param xsrfToken: string} /** Security token. */
|
||||
{@param username: string} /** Arbitrary username to display. */
|
||||
{@param logoutUrl: string} /** Generated URL for logging out of Google. */
|
||||
{@param productName: string} /** Name to display for this software product. */
|
||||
{@param logoFilename: string}
|
||||
{@param? errorMessage: string} /** If set - display the error message above the form. */
|
||||
{@param? baseClientId: string} /** If set - an initial value to fill for the base client ID. */
|
||||
{@param? contactEmail: string} /** If set - an initial value to fill for the email. */
|
||||
|
||||
{call registry.soy.console.header}
|
||||
{param app: 'registrar' /}
|
||||
{param subtitle: 'OT&E setup' /}
|
||||
{/call}
|
||||
{call registry.soy.console.googlebar data="all" /}
|
||||
<div id="reg-content-and-footer">
|
||||
<div id="reg-content">
|
||||
<h1>OT&E Setup Page</h1>
|
||||
{if isNonnull($errorMessage)}
|
||||
<h2 class="{css('kd-errormessage')}">Failed: {$errorMessage}</h2>
|
||||
{/if}
|
||||
{call .form_ data="all" /}
|
||||
</div>
|
||||
{call registry.soy.console.footer /}
|
||||
</div>
|
||||
{/template}
|
||||
|
||||
|
||||
/**
|
||||
* Result page for a successful OT&E setup.
|
||||
*/
|
||||
{template .resultSuccess}
|
||||
{@param baseClientId: string} /** The base clientId used for the OT&E setup. */
|
||||
{@param contactEmail: string} /** The contact's email added to the registrars. */
|
||||
{@param clientIdToTld: map<string, string>} /** The created registrars->TLD mapping. */
|
||||
{@param password: string} /** The password given for the created registrars. */
|
||||
{@param username: string} /** Arbitrary username to display. */
|
||||
{@param logoutUrl: string} /** Generated URL for logging out of Google. */
|
||||
{@param productName: string} /** Name to display for this software product. */
|
||||
{@param logoFilename: string}
|
||||
|
||||
{call registry.soy.console.header}
|
||||
{param app: 'registrar' /}
|
||||
{param subtitle: 'OT&E setup' /}
|
||||
{/call}
|
||||
{call registry.soy.console.googlebar data="all" /}
|
||||
<div id="reg-content-and-footer">
|
||||
<div id="reg-content" class="{css('item')} {css('registrar')}">
|
||||
<h1>OT&E successfully created for registrar {$baseClientId}!</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="{css('setting-label')}">EPP credentials</label>
|
||||
<span class="{css('description')}">Copy and paste this into an email to the registrars</span>
|
||||
</td>
|
||||
<td class="{css('setting')}">
|
||||
<textarea rows="7" cols="100" readonly>
|
||||
{for $clientId in mapKeys($clientIdToTld)}
|
||||
Login: {$clientId}{sp}
|
||||
Password: {$password}{sp}
|
||||
Tld: {$clientIdToTld[$clientId]}{\n}
|
||||
{/for}
|
||||
</textarea>
|
||||
</td>
|
||||
</table>
|
||||
Gave <label>{$contactEmail}</label> web-console access to these registrars.
|
||||
<h1>Don't forget to set the <label>Certificate</label> and <label>IP-whitelist</label> for these Registrars!</h1>
|
||||
Links to the security page for your convenience:<br>
|
||||
{for $clientId in mapKeys($clientIdToTld)}
|
||||
<a href="/registrar?clientId={$clientId}#security-settings" target="_blank">{$clientId}</a><br>
|
||||
{/for}
|
||||
</div>
|
||||
{call registry.soy.console.footer /}
|
||||
</div>
|
||||
{/template}
|
||||
|
||||
|
||||
/** Form for getting registrar info. */
|
||||
{template .form_ visibility="private"}
|
||||
{@param xsrfToken: string} /** Security token. */
|
||||
{@param? baseClientId: string} /** If set - an initial value to fill for the base client ID. */
|
||||
{@param? contactEmail: string} /** If set - an initial value to fill for the email. */
|
||||
<form name="item" class="{css('item')}" method="post" action="/registrar-ote-setup">
|
||||
<table>
|
||||
{call registry.soy.forms.inputFieldRowWithValue}
|
||||
{param label: 'Base clientId' /}
|
||||
{param name: 'clientId' /}
|
||||
{param value: $baseClientId /}
|
||||
{param placeholder: 'registrar\'s ID' /}
|
||||
{param description kind="text"}
|
||||
must 1) consist of only lowercase letters, numbers, or hyphens,
|
||||
2) start with a letter, and 3) be between 3 and 14 characters (inclusive).
|
||||
We require 1 and 2 since the registrar name will be used to create TLDs,
|
||||
and we require 3 since we append \"-[1234]\" to the name to create client
|
||||
IDs which are required by the EPP XML schema to be between 3-16 chars.
|
||||
{/param}
|
||||
{param readonly: false /}
|
||||
{/call}
|
||||
{call registry.soy.forms.inputFieldRowWithValue}
|
||||
{param label: 'contact email' /}
|
||||
{param name: 'email' /}
|
||||
{param value: $contactEmail /}
|
||||
{param placeholder: 'registrar\'s assigned email' /}
|
||||
{param description kind="text"}
|
||||
Will be granted web-console access to the OTE registrars.
|
||||
{/param}
|
||||
{param readonly: false /}
|
||||
{/call}
|
||||
{call registry.soy.forms.submitRow}
|
||||
{param label: 'create' /}
|
||||
{/call}
|
||||
</table>
|
||||
<input type="hidden" name="xsrfToken" value="{$xsrfToken}">
|
||||
</form>
|
||||
{/template}
|
||||
|
||||
|
||||
/**
|
||||
* Who goes thar?!
|
||||
*/
|
||||
{template .whoareyou}
|
||||
{@param username: string} /** Arbitrary username to display. */
|
||||
{@param logoutUrl: string} /** Generated URL for logging out of Google. */
|
||||
{@param logoFilename: string}
|
||||
{@param productName: string}
|
||||
{call registry.soy.console.header}
|
||||
{param app: 'registrar' /}
|
||||
{param subtitle: 'Not Authorized' /}
|
||||
{/call}
|
||||
<div class="{css('whoAreYou')}">
|
||||
<a class="{css('logo')}" href="/registrar">
|
||||
<img src="/assets/images/{$logoFilename}" alt="{$productName}">
|
||||
</a>
|
||||
<h1>You need permission</h1>
|
||||
<p>
|
||||
Only {$productName} Admins have access to this page.
|
||||
You are signed in as <strong>{$username}</strong>.
|
||||
<div>
|
||||
<a href="{$logoutUrl}"
|
||||
class="{css('kd-button')} {css('kd-button-submit')}"
|
||||
tabindex="-1">Logout and switch to another account</a>{sp}
|
||||
<a href="/registrar"
|
||||
class="{css('kd-button')} {css('kd-button-submit')}"
|
||||
tabindex="-1">Go to the Registrar web console</a>
|
||||
</div>
|
||||
</div>
|
||||
{/template}
|
||||
|
||||
|
|
@ -27,7 +27,6 @@
|
|||
<label class="{css('setting-label')}">Allowed TLDs</label>
|
||||
<span class="{css('description')}">set or remove TLDs this
|
||||
client is allowed access to.</span>
|
||||
</td>
|
||||
<td class="{css('setting')}">
|
||||
<div class="{css('info')} {css('summary')}">
|
||||
<div id="tlds">
|
||||
|
@ -44,8 +43,11 @@
|
|||
class="{css('kd-button')} {css('btn-add')}">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<tr class="{css('kd-settings-pane-section')}">
|
||||
<td>
|
||||
<label class="{css('setting-label')}">OT&E setup page</label>
|
||||
<td class="{css('setting')}">
|
||||
<p>Generate new OT&E accounts <a href="/registrar-ote-setup">here</a>
|
||||
</table>
|
||||
</form>
|
||||
{/template}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue