001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.users;
019
020import com.fasterxml.jackson.annotation.JsonView;
021import io.stallion.Context;
022import io.stallion.assets.*;
023import io.stallion.dataAccess.filtering.FilterChain;
024import io.stallion.dataAccess.filtering.Pager;
025import io.stallion.exceptions.*;
026import io.stallion.requests.validators.SafeMerger;
027import io.stallion.restfulEndpoints.*;
028import io.stallion.settings.Settings;
029import io.stallion.templating.TemplateRenderer;
030import io.stallion.utils.Sanitize;
031import io.stallion.utils.json.RestrictedViews;
032import org.apache.commons.lang3.StringUtils;
033
034import javax.ws.rs.*;
035
036import java.io.UnsupportedEncodingException;
037import java.net.URL;
038import java.net.URLEncoder;
039import java.util.Map;
040
041import static io.stallion.utils.Literals.*;
042import static io.stallion.Context.*;
043
044@Path("/st-users")
045public class UsersApiResource implements EndpointResource {
046
047    public static void register() {
048        if (Settings.instance().getUsers().getEnableDefaultEndpoints()) {
049            EndpointsRegistry.instance().addResource("", new UsersApiResource());
050
051            /*
052            DefinedBundle.register(new DefinedBundle(
053                    "userAdminStylesheets", ".css",
054
055                    new BundleFile().setPluginName("stallion").setLiveUrl("admin/admin.css"),
056                    new BundleFile().setPluginName("stallion").setLiveUrl("admin/users-manage.css")
057            ));
058            DefinedBundle.register(new DefinedBundle(
059                    "userAdminJavascripts", ".js",
060                    new BundleFile().setPluginName("stallion").setLiveUrl("admin/users-manage.js"),
061                    new BundleFile().setPluginName("stallion").setLiveUrl("admin/users-table-riot.tag").setProcessor("riot")
062            ));
063
064
065            BundleRegistry.instance().register(
066                    new CompiledBundle("user-admin-vue",
067                            new ResourceBundleFile("stallion", "admin/admin.css"),
068                            new ResourceBundleFile("stallion", "admin/users-manage.css"),
069                            new ResourceBundleFile("stallion", "vendor/vue.min.js", "vendor/vue.js"),
070                            new ResourceBundleFile("stallion", "vendor/vue-router.min.js", "vendor/vue-router.js"),
071                            new VueResourceBundleFile("stallion", "admin/*.vue"),
072                            new ResourceBundleFile("stallion", "admin/users-manage-v2.js")
073                    )
074            );
075
076*/
077            /*
078            DefinedBundle.register(
079                    "user-admin-vue",
080                    new BundleFile().setPluginName("stallion").setLiveUrl("admin/admin.css"),
081                    new BundleFile().setPluginName("stallion").setLiveUrl("admin/users-manage.css"),
082                    new BundleFile().setPluginName("stallion").setLiveUrl("vendor/vue.min.js").setDebugUrl("vendor/vue.js"),
083                    new BundleFile().setPluginName("stallion").setLiveUrl("vendor/vue-router.min.js").setDebugUrl("vendor/vue-router.js"),
084                    new VueBundleFile().setPluginName("stallion").setLiveUrl("admin/users-table.vue"),
085                    new VueBundleFile().setPluginName("stallion").setLiveUrl("admin/users-edit.vue"),
086                    new BundleFile().setPluginName("stallion").setLiveUrl("admin/users-manage-v2.js")
087            );   */
088        }
089    }
090
091    @GET
092    @Path("/login")
093    @Produces("text/html")
094    public Object loginScreen(@QueryParam("email") String email) {
095        URL url = getClass().getResource("/templates/public/login.jinja");
096        email = or(email, "");
097        Map ctx = null;
098        try {
099            ctx = map(
100                    val("allowReset", settings().getUsers().getPasswordResetEnabled()),
101                    val("allowRegister", settings().getUsers().getNewAccountsAllowCreation()),
102                    val("returnUrl", URLEncoder.encode((or(request().getParameter("stReturnUrl"), "")).replace("\"", ""), "UTF-8")),
103                    val("email", Sanitize.escapeHtmlAttribute(email)));
104        } catch (UnsupportedEncodingException e) {
105           throw new RuntimeException(e);
106        }
107        String html = TemplateRenderer.instance().renderTemplate(url.toString(), ctx);
108        return html;
109    }
110
111    @POST
112    @Path("/valet-login")
113    @Produces("application/json")
114    @MinRole(Role.MEMBER)
115    public Object valetLogin(@BodyParam("targetUser") Object userKey) {
116        if (StringUtils.isNumeric(userKey.toString())) {
117            UserController.instance().valetLoginIfAllowed(Long.parseLong(userKey.toString()));
118        } else {
119            UserController.instance().valetLoginIfAllowed(userKey.toString());
120        }
121        return true;
122    }
123
124    @GET
125    @Path("/logoff")
126    @Produces("text/html")
127    public Object logoff() {
128        UserController.instance().logoff();
129        throw new RedirectException(Settings.instance().getUsers().getLoginPage(), 302);
130    }
131
132    @POST
133    @JsonView(RestrictedViews.Owner.class)
134    @Produces("application/json")
135    @Path("/submit-login")
136    public Object login(@BodyParam("username") String username, @BodyParam("password") String password, @BodyParam(value = "rememberMe", allowEmpty = true) Boolean rememberMe) {
137        return UserController.instance().loginUser(username, password, rememberMe);
138    }
139
140    @GET
141    @Produces("text/html")
142    @Path("/register")
143    @MinRole(Role.ANON)
144    public String registerPage() {
145        if (!settings().getUsers().getNewAccountsAllowCreation()) {
146            throw new ClientException("The default new account creation endpoint is not enabled for this site.");
147        }
148        Map ctx = map();
149        return TemplateRenderer.instance().renderTemplate("stallion:/public/register.jinja", ctx);
150    }
151
152    @POST
153    @Produces("application/json")
154    @JsonView(RestrictedViews.Member.class)
155    @Path("/do-register")
156    public Object doRegister(@BodyParam("displayName") String displayName, @BodyParam("username") String email, @BodyParam("password") String password, @BodyParam("passwordConfirm") String passwordConfirm, @BodyParam(value = "returnUrl", allowEmpty = true) String returnUrl) {
157        if (!settings().getUsers().getNewAccountsAllowCreation()) {
158            throw new ClientException("The default new account creation endpoint is not enabled for this site.");
159        }
160        IUser existing = UserController.instance().forEmail(email);
161        if (existing != null) {
162            throw new ClientException("A user with that email address already exists.");
163        }
164        User user = new User()
165                .setDisplayName(displayName)
166                .setUsername(email)
167                .setRole(Role.valueOf(settings().getUsers().getNewAccountsRole().toUpperCase()))
168                .setEmail(email);
169
170        if (!settings().getUsers().getNewAccountsRequireValidEmail() && settings().getUsers().getNewAccountsAutoApprove() == true) {
171            user.setApproved(true);
172        }
173        Boolean requireValidEmail = false;
174        if (settings().getUsers().getNewAccountsRequireValidEmail()) {
175            requireValidEmail = true;
176        }
177        UserController.instance().hydratePassword(user, password, passwordConfirm);
178        IUser u = UserController.instance().createUser(user);
179        UserController.instance().addSessionCookieForUser(user, true);
180        if (requireValidEmail) {
181            UserController.instance().sendEmailVerifyEmail(user, or(returnUrl, ""));
182        }
183        return map(val("user", u), val("requireValidEmail", requireValidEmail));
184    }
185
186    @POST
187    @Produces("application/json")
188    @JsonView(RestrictedViews.Member.class)
189    @MinRole(Role.ADMIN)
190    @Path("/admin-create-user")
191    public Object adminCreateUser(@ObjectParam User newUser) {
192        if (empty(newUser.getEmail())) {
193            newUser.setEmail(newUser.getUsername());
194        } else if (empty(newUser.getUsername())) {
195            newUser.setUsername(newUser.getEmail());
196        }
197        IUser user = SafeMerger.with()
198                .nonEmpty("email", "username", "displayName", "role")
199                .optional("familyName", "givenName")
200                .merge(newUser);
201        IUser existing = UserController.instance().forEmail(user.getEmail());
202        if (existing != null) {
203            throw new ClientException("A user with that email address already exists.");
204        }
205
206        user.setApproved(true);
207        UserController.instance().createUser(user);
208        return user;
209    }
210
211
212
213    @POST
214    @Path("/send-verify-email")
215    @Produces("text/html")
216    public Object sendVerifyEmail(@BodyParam("email") String email, @BodyParam(value = "returnUrl", allowEmpty = true) String returnUrl) {
217        UserController.instance().sendEmailVerifyEmail(email, returnUrl);
218        return true;
219    }
220
221    @GET
222    @Path("/verify-email")
223    @Produces("text/html")
224    public Object verifyEmailAddress( @QueryParam("email") String email, @QueryParam("returnUrl") String returnUrl, @QueryParam("alreadySent") Boolean alreadySent) {
225
226        email = or(email, Context.getUser().getEmail());
227        alreadySent = or(alreadySent, false);
228        Map ctx = map(val("email", Sanitize.stripAll(email)), val("alreadySent", alreadySent));
229        String html = TemplateRenderer.instance().renderTemplate("stallion:/public/verify-email-address.jinja", ctx);
230        return html;
231    }
232
233    @GET
234    @Path("/verify-email-address")
235    @Produces("text/html")
236    public Object verifyEmailAddress(@QueryParam("verifyToken") String verifyToken, @QueryParam("email") String email, @QueryParam("returnUrl") String returnUrl) {
237
238        // For security, only verify an email address if we are logged in as that user
239        boolean requiresLogin = false;
240
241        if (empty(getUser().getId())) {
242            requiresLogin = true;
243        } else {
244            IUser associatedUser = UserController.instance().forEmail(email);
245            if (associatedUser == null || !associatedUser.getId().equals(getUser().getId())) {
246                requiresLogin = true;
247            }
248        }
249        if (requiresLogin) {
250            try {
251                String loginUrl = settings().getUsers().getFullLoginUrl() + "?email=" + email +
252                        "&returnUrl=" + URLEncoder.encode(request().requestUrl(), "UTF-8");
253                throw new RedirectException(loginUrl, 302);
254            } catch (UnsupportedEncodingException e) {
255                throw new RuntimeException(e);
256            }
257        }
258
259
260        Map<String, Object> ctx = map(val("verified", false), val("verificationFailed", false));
261        if (!empty(verifyToken) && !empty(email)) {
262            boolean verified = UserController.instance().verifyEmailVerifyToken(email, verifyToken);
263            if (verified) {
264                UserController.instance().markEmailVerified(email, verifyToken);
265                ctx.put("verified", true);
266            } else {
267                ctx.put("verificationFailed", true);
268            }
269        }
270        if (empty(email) && !empty(Context.getUser().getEmail())) {
271            email = Context.getUser().getEmail();
272        }
273        IUser user = UserController.instance().forUniqueKey("email", email);
274        if (user.getEmailVerified()) {
275            ctx.put("verified", true);
276            if (!user.getApproved()) {
277                ctx.put("requiresApproval", true);
278            }
279
280        }
281
282        returnUrl = or(returnUrl, Settings.instance().getSiteUrl());
283        ctx.put("returnUrl", Sanitize.stripAll(returnUrl));
284        ctx.put("email", Sanitize.stripAll(email));
285        URL url = getClass().getResource("/templates/public/verify-email-address.jinja");
286        String html = TemplateRenderer.instance().renderTemplate(url.toString(), ctx);
287        return html;
288    }
289
290
291
292    @GET
293    @Path("/reset-password")
294    @Produces("text/html")
295    public Object resetPassword(@QueryParam("resetToken") String resetToken, @QueryParam("email") String email, @QueryParam("returnUrl") String returnUrl) {
296        Map<String, Object> ctx = map(val("email", email), val("tokenVerified", false));
297        if (!empty(resetToken) && !empty(email)) {
298            boolean verified = UserController.instance().verifyPasswordResetToken(email, resetToken);
299            if (verified) {
300                ctx.put("tokenVerified", verified);
301            }
302        }
303        URL url = getClass().getResource("/templates/public/reset-password.jinja");
304        String html = TemplateRenderer.instance().renderTemplate(url.toString(), ctx);
305        return html;
306    }
307
308
309    @POST
310    @Path("/send-reset-email")
311    @Produces("application/json")
312    public Object sendResetEmail(@BodyParam("email") String email, @BodyParam(value = "returnUrl", allowEmpty = true) String returnUrl) {
313        if (!settings().getUsers().getPasswordResetEnabled()) {
314            throw new ClientException("Password reset has been disabled. Please contact an administrator to reset your password.");
315        }
316        IUser user = UserController.instance().forEmail(email);
317        if (user == null) {
318            user = UserController.instance().forUsername(email);
319        }
320        UserController.instance().sendPasswordResetEmail(user, returnUrl);
321        return true;
322    }
323
324
325    @POST
326    @Path("/do-password-reset")
327    @Produces("application/json")
328    public Object doPasswordReset(
329            @BodyParam("resetToken") String resetToken,
330            @BodyParam("email") String email,
331            @BodyParam("password") String password,
332            @BodyParam("passwordConfirm") String passwordConfirm
333    ) {
334
335        IUser user = UserController.instance().changePassword(email, resetToken, password, passwordConfirm);
336        if (user == null) {
337            return false;
338        }
339        UserController.instance().addSessionCookieForUser(user, true);
340        return true;
341    }
342
343    /*
344
345    @POST
346    @Path("/create-new-account")
347    @Produces("application/json")
348    public Object createNewAccount(@BodyParam("displayName") String displayName, @BodyParam(value = "email", isEmail = true) String email, @BodyParam(value = "password", minLength = 6) String password, @BodyParam(value = "passwordConfirm", minLength = 6) String passwordConfirm) {
349        if (!settings().getUsers().getNewAccountsAllowCreation()) {
350            throw new ClientException("User creation is disabled for this application.");
351        }
352
353        String domain = StringUtils.split(email, "@", 2)[1];
354        if (!empty(settings().getUsers().getNewAccountsDomainRestricted())) {
355            if (!settings().getUsers().getNewAccountsDomainRestricted().equals(domain)) {
356                throw new ClientException("You can only register email accounts from domain " + domain);
357            }
358        }
359
360        IUser user = UserController.instance().forEmail(email);
361        if (user == null) {
362            throw new ClientException("User with that email address already exists.");
363        }
364        user = new User();
365        user
366                .setDisplayName(displayName)
367                .setEmail(email);
368
369
370        if (!empty(settings().getUsers().getNewAccountsRole())) {
371            user.setRole(Role.valueOf(settings().getUsers().getNewAccountsRole()));
372        }
373
374        if (settings().getUsers().getNewAccountsAutoApprove()) {
375            user.setApproved(true);
376        }
377
378        UserController.instance().hydratePassword(user, password, passwordConfirm);
379        UserController.instance().createUser(user);
380
381        Map<String, Object> ctx = map(val("verifyEmailSent", false), val("pendingAdminApproval", false));
382        if (!settings().getUsers().getNewAccountsAutoApprove()) {
383            ctx.put("pendingAdminApproval", true);
384        }
385
386        if (settings().getUsers().getNewAccountsRequireValidEmail()) {
387            UserController.instance().sendEmailVerifyEmail(user, "");
388            ctx.put("verifyEmailSent", true);
389        }
390
391
392        UserController.instance().addSessionCookieForUser(user, true);
393
394        return map(val("verifyEmailSent", true), val("pendingAdminApproval", false));
395
396
397    }
398
399         */
400
401    @GET
402    @Path("/current-user-info")
403    @JsonView(RestrictedViews.Owner.class)
404    public Object currentUserInfo() {
405        if (Context.getUser() == null || empty(Context.getUser().getId())) {
406            throw new ClientException("You are not logged in", 401);
407        }
408        return Context.getUser();
409    }
410
411
412    /******
413     *
414     * ADMIN Endpoints
415     *
416     */
417
418
419
420    @GET
421    @Path("/manage")
422    @MinRole(Role.ADMIN)
423    @Produces("text/html")
424    public String manageUsers2() {
425
426        response().getMeta().setTitle("Manage Users");
427        Map<String, Object> ctx = map();
428        return TemplateRenderer.instance().renderTemplate("stallion:admin/admin-users2.jinja", ctx);
429    }
430
431
432    @GET
433    @Path("/users-screen")
434    @MinRole(Role.ADMIN)
435    @Produces("application/json")
436    public Map manageUsersScreen() {
437        Map<String, Object> ctx = map();
438        return ctx;
439    }
440
441    @GET
442    @Path("/users-table")
443    @MinRole(Role.ADMIN)
444    @Produces("application/json")
445    @JsonView(RestrictedViews.Owner.class)
446    public Pager usersTable(@QueryParam("page") Integer page, @QueryParam("withDeleted") Boolean withDeleted) {
447        Map<String, Object> ctx = map();
448        if (page == null || page < 1) {
449            page = 1;
450        }
451        withDeleted = or(withDeleted, false);
452        FilterChain chain = UserController.instance().filterChain();
453        if (withDeleted) {
454            chain = chain.includeDeleted();
455        }
456        Pager pager = chain.sort("email", "ASC").pager(page, 100);
457        return pager;
458    }
459
460
461    @GET
462    @Path("/view-user/:userId")
463    @MinRole(Role.ADMIN)
464    @Produces("application/json")
465    @JsonView(RestrictedViews.Owner.class)
466    public IUser viewUser(@PathParam("userId") Long userId) {
467        IUser user = UserController.instance().forIdWithDeleted(userId);
468        if (user == null) {
469            throw new io.stallion.exceptions.NotFoundException("User not found");
470        }
471        return user;
472    }
473
474    @POST
475    @Path("/update-user/:userId")
476    @MinRole(Role.ADMIN)
477    @Produces("application/json")
478    @JsonView(RestrictedViews.Owner.class)
479    public IUser updateUser(@PathParam("userId") Long userId, @ObjectParam(targetClass=User.class) User updatedUser) {
480        IUser user = UserController.instance().forIdWithDeleted(userId);
481        if (user == null) {
482            throw new io.stallion.exceptions.NotFoundException("User not found");
483        }
484        if (!updatedUser.getEmail().equals(user.getEmail())) {
485            IUser existing = UserController.instance().forEmail(updatedUser.getEmail());
486            if (existing != null) {
487                throw new ClientException("User with email " + updatedUser.getEmail() + " already exists");
488            }
489            user.setEmailVerified(false);
490        }
491        if (!updatedUser.getUsername().equals(user.getUsername())) {
492            IUser existing = UserController.instance().forUsername(updatedUser.getUsername());
493            if (existing != null) {
494                throw new ClientException("User with username " + updatedUser.getUsername() + " already exists");
495            }
496        }
497        user
498                .setDisplayName(updatedUser.getDisplayName())
499                .setUsername(updatedUser.getUsername())
500                .setEmail(updatedUser.getEmail())
501                .setRole(updatedUser.getRole())
502                .setFamilyName(updatedUser.getFamilyName())
503                .setGivenName(updatedUser.getGivenName());
504
505        UserController.instance().save(user);
506
507
508        return user;
509    }
510
511    @POST
512    @Path("/toggle-user-disabled/:userId")
513    @MinRole(Role.ADMIN)
514    @Produces("application/json")
515    @JsonView(RestrictedViews.Owner.class)
516    public Object toggleDisableUser(@PathParam("userId") Long userId, @BodyParam("disabled") boolean disabled) {
517        IUser user = UserController.instance().forIdWithDeleted(userId);
518        if (user == null) {
519            throw new io.stallion.exceptions.NotFoundException("User not found");
520        }
521        user.setDisabled(disabled);
522        UserController.instance().save(user);
523        return true;
524    }
525
526    @POST
527    @Path("/toggle-user-approved/:userId")
528    @MinRole(Role.ADMIN)
529    @Produces("application/json")
530    @JsonView(RestrictedViews.Owner.class)
531    public Object toggleUserApproved(@PathParam("userId") Long userId, @BodyParam("approved") boolean approved) {
532        IUser user = UserController.instance().forIdWithDeleted(userId);
533        if (user == null) {
534            throw new io.stallion.exceptions.NotFoundException("User not found");
535        }
536        user.setApproved(approved);
537        UserController.instance().save(user);
538        return true;
539    }
540
541    @POST
542    @Path("/toggle-user-deleted/:userId")
543    @MinRole(Role.ADMIN)
544    @Produces("application/json")
545    @JsonView(RestrictedViews.Owner.class)
546    public Object toggleUserDeleted(@PathParam("userId") Long userId, @BodyParam("deleted") boolean deleted) {
547        IUser user = UserController.instance().forIdWithDeleted(userId);
548        if (user == null) {
549            throw new io.stallion.exceptions.NotFoundException("User not found");
550        }
551        if (deleted == true) {
552            UserController.instance().softDelete(user);
553        } else {
554            user.setDeleted(deleted);
555            UserController.instance().save(user);
556        }
557
558        return true;
559    }
560
561    @POST
562    @Path("/force-password-reset/:userId")
563    @MinRole(Role.ADMIN)
564    @Produces("application/json")
565    @JsonView(RestrictedViews.Owner.class)
566    public Object triggerPasswordReset(@PathParam("userId") Long userId) {
567        IUser user = UserController.instance().forIdWithDeleted(userId);
568        if (user == null) {
569            throw new io.stallion.exceptions.NotFoundException("User not found");
570        }
571        user.setBcryptedPassword("");
572        UserController.instance().save(user);
573        UserController.instance().sendPasswordResetEmail(user, "");
574        return true;
575    }
576
577    @Path("/selenium/get-reset-token")
578    @GET
579    @Produces("application/json")
580    public Map getResetToken(@QueryParam("email") String email, @QueryParam("secret") String secret) {
581        if (!Settings.instance().getHealthCheckSecret().equals(secret)) {
582            throw new ClientException("Invalid or missing ?secret= query param. Secret must equal the healthCheckSecret in settings.");
583        }
584        if (!email.startsWith("selenium+resettest+") || !email.endsWith("@stallion.io")) {
585            throw new ClientException("Invalid email address. Must be a stallion selenium email.");
586        }
587        IUser user = UserController.instance().forEmail(email);
588        String token = UserController.instance().makeEncryptedToken(user, "reset", user.getResetToken());
589        return map(val("resetToken", token));
590    }
591
592}