Skip to content

Commit 41e92de

Browse files
init
1 parent 3932d11 commit 41e92de

File tree

9 files changed

+180
-21
lines changed

9 files changed

+180
-21
lines changed

backend/src/main/java/io/easystartup/suggestfeature/MongoTemplateFactory.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,18 @@
99
import io.easystartup.suggestfeature.loggers.Logger;
1010
import io.easystartup.suggestfeature.loggers.LoggerFactory;
1111
import io.easystartup.suggestfeature.utils.Util;
12+
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
13+
import org.springframework.context.event.ContextRefreshedEvent;
14+
import org.springframework.context.event.EventListener;
15+
import org.springframework.core.type.filter.AnnotationTypeFilter;
16+
import org.springframework.data.mapping.context.MappingContext;
1217
import org.springframework.data.mongodb.core.MongoTemplate;
18+
import org.springframework.data.mongodb.core.index.IndexOperations;
19+
import org.springframework.data.mongodb.core.index.IndexResolver;
20+
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver;
21+
import org.springframework.data.mongodb.core.mapping.Document;
22+
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
23+
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
1324
import org.springframework.stereotype.Service;
1425

1526
/*
@@ -22,6 +33,29 @@ public class MongoTemplateFactory {
2233

2334
private static MongoTemplate mongoTemplate;
2435

36+
@EventListener(ContextRefreshedEvent.class)
37+
public void initIndicesAfterStartup() {
38+
LOGGER.error("Creating indices");
39+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext = getMongoTemplate().getConverter().getMappingContext();
40+
41+
IndexResolver resolver = new MongoPersistentEntityIndexResolver(mappingContext);
42+
43+
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
44+
provider.addIncludeFilter(new AnnotationTypeFilter(Document.class));
45+
provider.findCandidateComponents(Main.class.getPackage().getName()).forEach(beanDefinition -> {
46+
try {
47+
Class<?> componentClass = Class.forName(beanDefinition.getBeanClassName());
48+
LOGGER.error("Creating indices for " + componentClass.getName());
49+
IndexOperations indexOps = getDefaultMongoTemplate().indexOps(componentClass);
50+
resolver.resolveIndexFor(componentClass).forEach(indexOps::ensureIndex);
51+
} catch (ClassNotFoundException e) {
52+
LOGGER.error("Classnotfound ", e);
53+
throw new RuntimeException(e);
54+
}
55+
});
56+
LOGGER.error("Indices created");
57+
}
58+
2559
public MongoTemplate getDefaultMongoTemplate() {
2660
return getMongoTemplate();
2761
}

backend/src/main/java/io/easystartup/suggestfeature/beans/Member.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212
@Document
1313
@JsonIgnoreProperties(ignoreUnknown = true)
14+
@CompoundIndex(name = "userId_1_organizationId_1", def = "{'userId': 1, 'organizationId': 1}", unique = true)
1415
public class Member {
1516

1617
public enum Role {

backend/src/main/java/io/easystartup/suggestfeature/beans/Organization.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public class Organization {
1919
@Id
2020
private String id;
2121
private String name;
22-
@Indexed
22+
@Indexed(unique = true)
2323
private String slug;
2424
private Long createdAt;
2525

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,35 @@
11
package io.easystartup.suggestfeature.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import jakarta.validation.constraints.NotBlank;
5+
26
/*
37
* @author indianBond
48
*/
9+
@JsonIgnoreProperties(ignoreUnknown = true)
510
public class OrganizationRequest {
11+
12+
@NotBlank
13+
private String organizationName;
14+
@NotBlank
15+
private String organizationSlug;
16+
17+
public OrganizationRequest() {
18+
}
19+
20+
public String getOrganizationName() {
21+
return organizationName;
22+
}
23+
24+
public void setOrganizationName(String organizationName) {
25+
this.organizationName = organizationName;
26+
}
27+
28+
public String getOrganizationSlug() {
29+
return organizationSlug;
30+
}
31+
32+
public void setOrganizationSlug(String organizationSlug) {
33+
this.organizationSlug = organizationSlug;
34+
}
635
}

backend/src/main/java/io/easystartup/suggestfeature/filters/AuthFilter.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import org.slf4j.MDC;
1515

1616
import java.io.IOException;
17+
import java.net.URLDecoder;
18+
import java.nio.charset.StandardCharsets;
1719
import java.util.Date;
1820

1921
import static io.easystartup.suggestfeature.FilterConfig.AUTH_FILTER_INCLUDED_INIT_PARAM_KEY;
@@ -59,9 +61,9 @@ public void doFilter(ServletRequest request, ServletResponse response,
5961
Cookie[] cookies = httpServletRequest.getCookies();
6062
for (Cookie cookie : cookies) {
6163
if ("token".equals(cookie.getName())) {
62-
token = cookie.getValue();
64+
token = URLDecoder.decode(cookie.getValue(), StandardCharsets.UTF_8);
65+
break;
6366
}
64-
break;
6567
}
6668
}
6769

backend/src/main/java/io/easystartup/suggestfeature/rest/UserRestApi.java

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@
22

33
import io.easystartup.suggestfeature.AuthService;
44
import io.easystartup.suggestfeature.MongoTemplateFactory;
5+
import io.easystartup.suggestfeature.ValidationService;
56
import io.easystartup.suggestfeature.beans.Member;
7+
import io.easystartup.suggestfeature.beans.Organization;
68
import io.easystartup.suggestfeature.beans.User;
9+
import io.easystartup.suggestfeature.dto.OrganizationRequest;
710
import io.easystartup.suggestfeature.filters.UserContext;
811
import io.easystartup.suggestfeature.utils.JacksonMapper;
9-
import jakarta.ws.rs.Consumes;
10-
import jakarta.ws.rs.GET;
11-
import jakarta.ws.rs.Path;
12-
import jakarta.ws.rs.Produces;
12+
import jakarta.ws.rs.*;
1313
import jakarta.ws.rs.core.Response;
14-
import org.apache.commons.lang3.StringUtils;
1514
import org.springframework.beans.factory.annotation.Autowired;
1615
import org.springframework.data.mongodb.core.query.Criteria;
1716
import org.springframework.data.mongodb.core.query.Query;
@@ -26,11 +25,13 @@ public class UserRestApi {
2625

2726
private final MongoTemplateFactory mongoConnection;
2827
private final AuthService authService;
28+
private final ValidationService validationService;
2929

3030
@Autowired
31-
public UserRestApi(MongoTemplateFactory mongoConnection, AuthService authService) {
31+
public UserRestApi(MongoTemplateFactory mongoConnection, AuthService authService, ValidationService validationService) {
3232
this.mongoConnection = mongoConnection;
3333
this.authService = authService;
34+
this.validationService = validationService;
3435
}
3536

3637
@GET
@@ -53,4 +54,46 @@ public Response getUser() {
5354
return Response.ok(JacksonMapper.toJson(safeUser)).build();
5455
}
5556

57+
@POST
58+
@Path("/create-org")
59+
@Consumes("application/json")
60+
@Produces("application/json")
61+
public Response createOrg(OrganizationRequest req) {
62+
validationService.validate(req);
63+
64+
// Set org slug based on the org name, all lower case and all special characters removed and spaces replaced with -
65+
// Also cant end with - or start with -
66+
// Example: "Example Org" => "example-org"
67+
// Example: "hello-how-do-you-do" => "hello-how-do-you-do"
68+
// Example: "-hello-how-do-you-do" => "hello-how-do-you-do"
69+
// Example: "-hello-how-do-you-do-" => "hello-how-do-you-do"
70+
// Limit max length to 35 characters
71+
String slug = req.getOrganizationSlug().trim().toLowerCase().replaceAll("[^a-z0-9\\s-]", "").replaceAll("[\\s-]+", "-").replaceAll("^-|-$", "");
72+
73+
slug = slug.substring(0, Math.min(slug.length(), 35));
74+
75+
req.setOrganizationSlug(slug);
76+
77+
// Ensure organization name is clean, does not contain xss and is trimmed
78+
req.setOrganizationName(req.getOrganizationName().trim());
79+
80+
String userId = UserContext.current().getUserId();
81+
Organization organization = new Organization();
82+
organization.setCreatedAt(System.currentTimeMillis());
83+
organization.setSlug(req.getOrganizationSlug());
84+
organization.setName(req.getOrganizationName());
85+
try {
86+
organization = mongoConnection.getDefaultMongoTemplate().insert(organization);
87+
Member member = new Member();
88+
member.setCreatedAt(System.currentTimeMillis());
89+
member.setUserId(userId);
90+
member.setOrganizationId(organization.getId());
91+
member.setRole(Member.Role.ADMIN);
92+
mongoConnection.getDefaultMongoTemplate().insert(member);
93+
} catch (Exception e) {
94+
return Response.status(Response.Status.BAD_REQUEST).entity("Organization Slug already exists").build();
95+
}
96+
return Response.ok(JacksonMapper.toJson(organization)).build();
97+
}
98+
5699
}

frontend/src/app/create-org/page.tsx

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,65 @@ import { Label } from "@/components/ui/label"
1414
import { useEffect, useState } from "react"
1515
import withAuth from "@/hoc/withAuth"
1616
import { log } from "console"
17+
import { API_BASE_URL, useAuth } from "@/context/AuthContext"
18+
import { Icons } from "@/components/icons"
19+
import { useRouter } from "next/navigation"
20+
import { useToast } from "@/components/ui/use-toast"
21+
import { ToastAction } from "@radix-ui/react-toast"
1722

1823
const CreateOrgForm: React.FC = ({ }) => {
1924
const [orgName, setOrgName] = useState('')
2025
const [orgSlug, setOrgSlug] = useState('')
26+
const [isLoading, setLoading] = useState(false)
27+
const router = useRouter();
28+
const { toast } = useToast();
2129

22-
useEffect(() => {
23-
// Set org slug based on the org name, all lower case and all special characters removed and spaces replaced with -
24-
// Example: "Example Org" => "example-org"
25-
// Limit max length to 20 characters
26-
setOrgSlug(orgName.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s/g, '-').slice(0, 35))
2730

28-
// allow to edit slug if user has entered manually
29-
if (orgSlug !== orgName.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s/g, '-').slice(0, 35)) {
30-
setOrgSlug(orgName.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s/g, '-').slice(0, 35))
31+
const submit = async () => {
32+
setLoading(true)
33+
const fixedOrgSlug = orgSlug.replace(/-$/g, '');
34+
setOrgSlug(fixedOrgSlug)
35+
const response = await fetch(`${API_BASE_URL}/api/auth/user/create-org`, {
36+
method: 'POST',
37+
headers: {
38+
'Content-Type': 'application/json',
39+
},
40+
// credentials: 'include',
41+
body: JSON.stringify({ organizationName: orgName, organizationSlug: fixedOrgSlug })
42+
});
43+
44+
if (response.ok) {
45+
const { slug } = await response.json();
46+
if (slug && slug.length > 0) {
47+
router.push(`/${slug}/dashboard`);
48+
} else {
49+
router.push(`/create-org`);
50+
}
51+
} else {
52+
toast({
53+
variant: "destructive",
54+
title: "Slug already exists. Try a different slug.",
55+
});
56+
57+
console.error('Slug already exists')
3158
}
3259

33-
}), [orgName]
60+
setTimeout(() => {
61+
setLoading(false)
62+
}, 1000)
63+
};
64+
65+
const updateSlug = (value: string) => {
66+
// Set org slug based on the org name, all lower case and all special characters removed and spaces replaced with -
67+
// Example: "Example Org" => "example-org"
68+
// Example: "Example Org" => "example-org"
69+
// Example: "hello-how-do-you-do" => "hello-how-do-you-do"
70+
// Example: "-hello-how-do-you-do" => "hello-how-do-you-do"
71+
// Example: "-hello-how-do-you-do-" => "hello-how-do-you-do"
72+
// Limit max length to 35 characters
73+
// replace all special characters with - and replace multiple - with single -
74+
setOrgSlug(value.toLowerCase().replace(/[^a-zA-Z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-/g, '').slice(0, 35))
75+
}
3476

3577
return (
3678
<Card className="mx-auto max-w-sm mt-20">
@@ -48,7 +90,10 @@ const CreateOrgForm: React.FC = ({ }) => {
4890
id="orgName"
4991
type="text"
5092
value={orgName}
51-
onChange={(e) => setOrgName(e.target.value)}
93+
onChange={(e) => {
94+
setOrgName(e.target.value)
95+
updateSlug(e.target.value)
96+
}}
5297
placeholder="Example Org"
5398
required
5499
/>
@@ -60,12 +105,15 @@ const CreateOrgForm: React.FC = ({ }) => {
60105
id="orgSlug"
61106
type="text"
62107
value={orgSlug}
63-
onChange={(e) => setOrgSlug(e.target.value)}
108+
onChange={(e) => updateSlug(e.target.value)}
64109
placeholder="example-org"
65110
required
66111
/>
67112
</div>
68-
<Button type="submit" className="w-full">
113+
<Button type="submit" className="w-full" disabled={isLoading} onClick={submit}>
114+
{isLoading && (
115+
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
116+
)}
69117
Create Organization
70118
</Button>
71119
</div>

frontend/src/app/favicon.ico

215 KB
Binary file not shown.

frontend/src/app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Metadata } from "next";
2+
import { Toaster } from "@/components/ui/toaster"
23
import { AuthProvider } from '../context/AuthContext';
34
import { Inter as FontSans } from "next/font/google"
45
import "./globals.css";
@@ -31,6 +32,7 @@ export default function RootLayout({
3132
<AuthProvider>
3233
{children}
3334
</AuthProvider>
35+
<Toaster />
3436
</body>
3537
</html>
3638
);

0 commit comments

Comments
 (0)