001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.petstore.console.users;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import org.apache.juneau.commons.beanquery.*;
022import org.apache.juneau.http.*;
023import org.apache.juneau.http.response.*;
024import org.apache.juneau.marshall.collections.*;
025import org.apache.juneau.petstore.console.*;
026import org.apache.juneau.petstore.console.pages.*;
027import org.apache.juneau.petstore.dto.*;
028import org.apache.juneau.rest.server.*;
029import org.apache.juneau.console.datatables.*;
030import org.apache.juneau.console.datatables.adapter.*;
031import org.apache.juneau.rest.server.view.*;
032import org.apache.juneau.rest.server.view.freemarker.*;
033import org.apache.juneau.console.views.*;
034import org.apache.juneau.console.widgets.*;
035
036/**
037 * The Users page and its endpoints.  The password is write-only: every read goes through {@link UserView}, which has
038 * no password, so it never leaves the server; an update with no password keeps the stored one.
039 *
040 * <h5 class='section'>Example:</h5>
041 * <p class='bjava'>
042 *    <jc>// The wire shape of a create (JSON5): the password is accepted but never echoed.</jc>
043 *    { username:<js>'newbie'</js>, firstName:<js>'New'</js>, lastName:<js>'Bie'</js>, password:<js>'s3cret'</js> }
044 * </p>
045 * <p class='bcode'>
046 *    &lt;@card type="datatables" id="users"&gt;
047 *    { dataMode:'server', dataUrl:'/console/users/query', columns:[{key:'username', label:'Username'}, ...] }
048 *    &lt;/@card&gt;
049 * </p>
050 */
051@Rest(path="/users", title="Users")
052@SuppressWarnings({
053   "java:S110", // Inheritance depth comes from the BasicRestServlet hierarchy, not this page.
054   "java:S1192" // Route paths and field names repeat as inline literals by design.
055})
056public class UsersRest extends PetstoreConsolePage {
057
058   private static final long serialVersionUID = 1L;
059
060   /** The query context over {@link UserView}, so the password is neither searchable nor serialized. */
061   private static final InMemoryBeanQueryContext<UserView> VIEW_QUERY = InMemoryBeanQueryContext.create(UserView.class).build();
062
063   /**
064    * A user as the console shows it: everything but the password.
065    *
066    * <h5 class='section'>Example:</h5>
067    * <p class='bcode'>
068    *    { username:'mwatson', firstName:'Mary', lastName:'Watson', userStatus:'ACTIVE' }
069    * </p>
070    *
071    * @param username The username (primary key).
072    * @param firstName The first name.
073    * @param lastName The last name.
074    * @param userStatus The account status.
075    */
076   public record UserView(String username, String firstName, String lastName, UserStatus userStatus) {
077
078      /**
079       * @param u The stored user.
080       * @return Its password-free view.
081       */
082      public static UserView of(User u) {
083         return new UserView(u.getUsername(), u.getFirstName(), u.getLastName(), u.getUserStatus());
084      }
085   }
086
087   /**
088    * The Users page.
089    *
090    * @param req The request; {@code ?builder} renders the builder twin.
091    * @return The page.
092    */
093   @RestGet(path="/")
094   public View page(RestRequest req) {
095      return builder(req) ? twin(req, UsersPageBuilders.users()) : FreemarkerView.of("users.ftlh");
096   }
097
098   /**
099    * The add/edit dialog form.  The password field is write-only: it is never pre-filled.
100    *
101    * @param username The user to edit, or <jk>null</jk> to add.
102    * @return The modal.
103    * @throws NotFound If {@code username} names no user.
104    */
105   @RestGet(path="/form")
106   public ModalDef form(@Query("username") String username) {
107      var u = username == null ? new User() : require(username);
108      var form = FormDef.create();
109      if (username == null)
110         form.field(FormDef.Input.of("username", "Username", "text").required());
111      form.field(FormDef.Input.of("firstName", "First name", "text").value(u.getFirstName()));
112      form.field(FormDef.Input.of("lastName", "Last name", "text").value(u.getLastName()));
113      var password = FormDef.Input.of("password", "Password", "password");
114      form.field(username == null ? password.required().help("Write-only; it is never shown again") : password.help("Leave blank to keep the current password"));
115      return ModalDef.create(username == null ? "Add user" : f("Edit '%s'", username)).form(form);
116   }
117
118   /**
119    * Server-mode DataTables query over all users, as password-free views.
120    *
121    * @param req The DataTables request.
122    * @return One page of users, or a 200 {@code error} envelope for a malformed search expression.
123    */
124   @RestPost(path="/query")
125   public DataTablesResults<UserView> query(@Content DataTablesRequest req) {
126      try (var s = VIEW_QUERY.getSession(store().getUsers().stream().map(UserView::of).toList())) {
127         return DataTablesQuery.run(req, s);
128      } catch (BeanQuerySyntaxException e) {
129         return DataTablesResults.error(req.getDraw(), e.getMessage());
130      }
131   }
132
133   /**
134    * @param username The username.
135    * @return The user, without the password.
136    * @throws NotFound If it does not exist.
137    */
138   @RestGet(path="/{username}")
139   public UserView get(@Path("username") String username) {
140      return UserView.of(require(username));
141   }
142
143   /**
144    * Creates a user.
145    *
146    * @param req The request (write gate, audit actor).
147    * @param body {@code {username, firstName, lastName, password}}, either plain or as the dialog envelope {@code {fields:{...}}}.
148    * @return The result toast, carrying the user without the password.
149    * @throws BadRequest On a blank username.
150    * @throws Conflict If the username is taken.
151    */
152   @RestPost(path="/")
153   public ActionResult create(RestRequest req, @Content JsonMap body) {
154      var actor = ConsoleWrites.actor(req);
155      var f = fields(body);
156      var username = f.getString("username");
157      if (ib(username))
158         throw new BadRequest("Username must not be blank");
159      if (store().getUser(username) != null)
160         throw new Conflict("User '%s' already exists", username);
161      var u = new User().setUsername(username).setFirstName(f.getString("firstName")).setLastName(f.getString("lastName"))
162         .setPassword(f.getString("password")).setUserStatus(status(f, UserStatus.ACTIVE));
163      store().createUser(u, actor);
164      return ActionResult.success(UserView.of(u)).message(f("Added '%s'", username));
165   }
166
167   /**
168    * Updates a user.  A field left out of the body, and a null or blank password, keep their stored values.
169    *
170    * @param req The request.
171    * @param username The username.
172    * @param body The new state, either plain or as the dialog envelope {@code {fields:{...}}}.
173    * @return The result toast, carrying the user without the password.
174    * @throws NotFound If the user does not exist.
175    */
176   @RestPut(path="/{username}")
177   public ActionResult update(RestRequest req, @Path("username") String username, @Content JsonMap body) {
178      var actor = ConsoleWrites.actor(req);
179      var old = require(username);
180      var f = fields(body);
181      var pw = f.getString("password");
182      var u = new User().setUsername(username)
183         .setFirstName(f.containsKey("firstName") ? f.getString("firstName") : old.getFirstName())
184         .setLastName(f.containsKey("lastName") ? f.getString("lastName") : old.getLastName())
185         .setEmail(old.getEmail()).setPhone(old.getPhone())
186         .setUserStatus(status(f, old.getUserStatus()))
187         .setPassword(pw == null || pw.isBlank() ? old.getPassword() : pw);
188      store().updateUser(u, actor);
189      return ActionResult.success(UserView.of(u)).message(f("Saved '%s'", username));
190   }
191
192   /**
193    * Deletes a user.
194    *
195    * @param req The request.
196    * @param username The username.
197    * @return The result toast.
198    * @throws NotFound If the user does not exist.
199    */
200   @RestDelete(path="/{username}")
201   public ActionResult delete(RestRequest req, @Path("username") String username) {
202      var actor = ConsoleWrites.actor(req);
203      require(username);
204      store().deleteUser(username, actor);
205      return ActionResult.success(null).message(f("Deleted '%s'", username));
206   }
207
208   private User require(String username) {
209      var u = store().getUser(username);
210      if (u == null)
211         throw new NotFound("Unknown user '%s'", username);
212      return u;
213   }
214
215   /** Unwraps the dialog envelope ({@code fields}) if present; otherwise the body is the form itself. */
216   private static JsonMap fields(JsonMap body) {
217      if (body == null)
218         return new JsonMap();
219      var f = body.getMap("fields");
220      return f != null ? f : body;
221   }
222
223   private static UserStatus status(JsonMap f, UserStatus dflt) {
224      var s = f.getString("userStatus");
225      if (ib(s))
226         return dflt;
227      try {
228         return UserStatus.valueOf(s);
229      } catch (IllegalArgumentException e) {
230         throw new BadRequest("Unknown user status '%s'", s);
231      }
232   }
233}