Code Monkey home page Code Monkey logo

integrations's Introduction

Integrations with Ory

This repository contains integrations for connecting with Ory Cloud.

Table of Contents

NextJS

To connect a NextJS app with Ory, do the following in your NextJS App:

$ npm i --save @ory/integrations

Then create a file at <your-nextjs-app>/api/.ory/[...paths].ts with the following contents:

import { config, createApiHandler } from "@ory/integrations/next-edge"

export { config }

export default createApiHandler({
  /* ... */
})

You need to set the environment variable ORY_SDK_URL to your Ory Cloud Project SDK URL. For a list of available options head over to src/nextjs/index.ts.

Vercel

To connect a non NextJS vercel app, do the following in your vercel app:

$ npm i --save @ory/integrations

Then create a file at <your-vercel-app>/api/oryproxy.js with the following contents:

import { config, createApiHandler } from "@ory/integrations/next-edge"

export { config }

const ah = createApiHandler({
  /* ... */
})
const apiHandlerWrapper = (req, res) => {
  req.query.paths = req.url.replace(/^\/api\/.ory\//, "").split("?")[0]
  ah(req, res)
}
export default apiHandlerWrapper

Then add the following contents to <your-vercel-app>/vercel.json:

{
    "rewrites": [
        { "source": "/api/.ory/:match*", "destination": "/api/oryproxy" }
    ]
}

SDK Helpers

This package contains several helpers for using the Ory SDKs with TypeScript, JavaScript, and NodeJS.

Type Guards

This package includes type guards for identifying UI nodes.

import {
  isUiNodeImageAttributes,
  isUiNodeInputAttributes,
  isUiNodeScriptAttributes,
  isUiNodeTextAttributes,
  // ...
} from "@ory/integrations/ui"

// ...

if (isUiNodeImageAttributes(node.attributes)) {
  console.log("it is an image: ", node.attributes.src)
}

UI Node Helpers

This package contains convenience functions for UI nodes:

  • import { getNodeLabel } from '@ory/integrations/ui': Returns the node's label.
  • import { getNodeId } from '@ory/integrations/ui': Returns a node's ID.
  • import { filterNodesByGroups } from '@ory/integrations/ui': Filters nodes by their groups.

An example of using the filterNodesByGroups function could be to map the UiNode[] to a certain JSX components.

The example below is from the ory/themes repository and is used to map out the UI Nodes to JSX Components.

Understanding filterNodesByGroups is quite easy if you think about it as a hierarchy:

const nodes = [
  {
    group: "webauthn",
    attributes: {
      node_type: "input",
      type: "input",
    },
  },
  {
    group: "oidc",
    attributes: {
      node_types: "button",
      type: "submit",
    },
  },
  {
    group: "oidc", //<-- take note here, we have 2 oidc groups
    attributes: {
      node_types: "input",
      type: "checkbox"
    }
  }
  {
    group: "foo",
    attributes: {
      node_types: "bar",
      type: "bar",
    },
  },
]

filterNodesByGroups({
  nodes: nodes,
  groups: "oidc,webauthn", //<-- filter these first
  attributes: "submit", // <-- then these will only take nodes containing the `submit` attributes
  withoutDefaultAttributes: true, //<-- dont add 'hidden' and 'script' fields when we specify attributes
  excludeAttributes: "checkbox", // <-- defining this wont do much here since we defined attributes. exclude the attributes to see what happens.
})

How will our output look like?

[
-  {
-    group: "webauthn",
-    attributes: {
-      node_type: "input",
-      type: "input",
-    },
-  },
+  {
+    group: "oidc",
+    attributes: {
+      node_types: "button",
+      type: "submit",
+    },
+  },
-  {
-    group: "oidc", //<-- take note here, we have 2 oidc groups
-    attributes: {
-      node_types: "input",
-      type: "checkbox"
-    }
-  }
-  {
-    group: "foo",
-    attributes: {
-      node_types: "bar",
-      type: "bar",
-    },
-  },
]

An example is we have a UINode containing the group "totp" and attributes node type "input".

{
  group: "totp",
  attributes: {
    name: "f",
    type: "input",
    node_type: "input",
  },
}

Our end goal is to map it to HTML, something like this.

<input type="input" name="f" />

To achieve that, we could wrap it in a nifty JSX component which returns the correct component based on our UI node type.

We accept a filter object which is basically the FilterNodesByGroups type and return a <Node /> component, which is a component that helps us return our specific HTML.

export const FilterFlowNodes = ({
  filter,
  includeCSRF,
}: {
  filter: FilterNodesByGroups
  includeCSRF?: boolean
}): JSX.Element | null => {
  const getInputName = (node: UiNode): string =>
    isUiNodeInputAttributes(node.attributes) ? node.attributes.name : ""

  // Here we are using our filterNodesByGroups to get the nodes we really want. We can even do some more filtering and mapping
  const nodes = filterNodesByGroups(filter)
    // we don't want to map the csrf token every time, only on the form level
    .filter((node) =>
      getInputName(node) === "csrf_token" && !includeCSRF ? false : true,
    )
    .map((node, k) =>
      ["hidden"].includes(getNodeInputType(node.attributes))
        ? {
            node: <Node node={node} key={k} />,
            hidden: true,
          }
        : {
            node: <Node node={node} key={k} />,
            hidden: false,
          },
    )

  return nodes.length > 0 ? (
    <>
      // we don't want hidden fields to create new gaps
      {nodes.filter((node) => node.hidden).map((node) => node.node)}
      <div className={gridStyle({ gap: 16 })}>
        {nodes.filter((node) => !node.hidden).map((node) => node.node)}
      </div>
    </>
  ) : null
}

Now we can use our wrapper to return the HTML we want based on the nodes.

Here we only want nodes that do not have the hidden attribute.

<FilterFlowNodes
  filter={{
    nodes: nodes,
    excludeAttributes: "hidden",
  }}
/>

Another more complex example is to filter out the UI nodes to only retrieve the oidc and password groups. We also exclude the default group here with withoutDefaultGroup: true. Furthermore we do some exclusions on the submit and hiddenattributes, so any group which has an attribute containing a node typesubmitorhidden` will be filtered out.

<FilterFlowNodes
  filter={{
    nodes: nodes,
    groups: ["oidc", "password"],
    withoutDefaultGroup: true,
    excludeAttributes: ["submit", "hidden"],
  }}
/>

Another example of us wanting the oidc and webauthn group (note: we can use comma seperated strings instead of an array). We also exclude default attributes with withoutDefaultAttributes: true which are hidden and script elements. This will also only return us the nodes which have a submit attribute.

<FilterFlowNodes
  filter={{
    nodes: flow.ui.nodes,
    groups: "oidc,webauthn",
    withoutDefaultAttributes: true,
    attributes: "submit",
  }}
/>

integrations's People

Contributors

aeneasr avatar afreakk avatar benehiko avatar fbnfgc avatar jgillich avatar jonas-jonas avatar k-yle avatar lstellway avatar xahy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

integrations's Issues

Support Netlify Functions for NPM Ory Cloud Integrations

See discussions https://ory-community.slack.com/archives/C02MR4DEEGH/p1646150967822979

Basically, what we do here

function getBaseUrl(options: CreateApiHandlerOptions) {

is that we create an expressJS handler that is specific to NextJS / Vercel's edge function model. However, under the hood, it's just plain expressJS (I believe).

If we extract the configuration options and the actuall proxying

return (req: NextApiRequest, res: NextApiResponse<string>) => {

we could for sure make it work with netlify and other providers (e.g. cloudflare functions).

It's probably just a bit of fiddling to get it right!

bug with cookie domain

hostname contains domain, so this condition returns false all time

if (!parsed.domain) { return parsed.hostname }

image

Cookie domain should per default use the TLD

As seen in ory/network#53 we should, per default, set the cookie on the TLD instead of the current domain. This will prevent CORS errors and is what we also have done in the Ory Cloud CNAME feature.

Similar to

/**
* Per default, this handler will strip the cookie domain from
* the Set-Cookie instruction which is recommended for most set ups.
*
* If you are running this app on a subdomain and you want the session and CSRF cookies
* to be valid for the whole TLD, you can use this setting to force a cookie domain.
*/
forceCookieDomain?: string

we should have an option

setOnTopLevelDomain

which defaults to true and will set the cookie on the top level domain (e.g. app running on www.example.org -> cookie set on example.org). This should of course skip any domains that are known to host public things such as oryapis.com or other public TLDs. We can use a library for that, e.g.: https://github.com/thom4parisot/tld.js

Package does not work well in browser environments

This module bundles both browser (ui node helpers) and server side (NextJS serverless) components. The problem is that we need request in serverless to be able to pipe/proxy requests. This however causes an issue with the bundle as net tls fs are required but require a webpack workaround on browser. For NextJS this is for example

/** @type {import('next').NextConfig} */
module.exports = {
  reactStrictMode: true,
  webpack: (config, { isServer }) => {
    // Some dependencies require 'net' module. We mock it on the client!
    if (!isServer) {
      config.resolve.fallback = {
        net: false,
        tls: false,
        fs: false
      }
    }
    return config
  }
}

if you import e.g. this in your frontend code:

import { ui } from '@ory/integrations'

This obviously sucks and the package should declare dependencies correctly. e.g. we don't use serverless for normal react apps - so why is it bundled in the front end code?

Solution would probably be different build targets or something to make this work?

peer dependency mismatch

Why is @ory/client specified both as a dependency and as a peerDependency? Not just that, but they have conflicting versions.

"peerDependencies": {
"@ory/client": "^0.2.0-alpha.16",
"next": ">=12.0.10"
},
"dependencies": {
"@ory/client": "^1.1.0",

Next.js middleware breaks api request

I'm trying to implement Self-Service login flow. I checked the repo mentioned in the documentation page (kratos-selfservice-ui-react-nextjs).

The project works fine until [email protected]. However after [email protected] it seems the changes introduce in the middleware logic, specifically this PR vercel/next.js#34519 breaks the integration with ory kratos. Where transforming the request body to web stream and convert it back to nodejs stream somehow breaks proxying requests of nexjs integration, which leads to send all requests as GET requests to kratos.

The request was malformed or contained invalid parameters reason:Unable to decode body because HTTP Request Method was "GET" but only [POST PUT PATCH] are supported

To reproduce the issue use kratos-selfservice-ui-react-nextjs and upgrade next to [email protected] or latest and add a basic middleware:

//pages/_middleware.ts
import {NextRequest, NextResponse} from 'next/server';

export default (req: NextRequest) => {
    return NextResponse.next();
}

From the browser network I can see the login POST request http://localhost:3000/api/.ory/self-service/login?flow={id} is responded with redirect (303) to http://localhost:3000/login?flow={id}

And this is the log from kratos instance:

time=2022-06-12T14:01:57Z level=info msg=started handling request func=github.com/ory/x/reqlog.(*Middleware).ServeHTTP file=/go/pkg/mod/github.com/ory/[email protected]/reqlog/middleware.go:131 http_request=map[headers:map[accept-encoding:gzip, deflate connection:close] host:localhost:4433 method:GET path:/self-service/login query:flow=40013f4d-bd56-4769-8d58-30598972762c remote:172.18.0.1:60604 scheme:http]
2022-06-12T14:01:57.086820106Z time=2022-06-12T14:01:57Z level=info msg=Encountered self-service login error. func=github.com/ory/kratos/selfservice/flow/login.(*ErrorHandler).WriteFlowError file=/project/selfservice/flow/login/error.go:78 audience=audit error=map[debug: message:The request was malformed or contained invalid parameters reason:Unable to decode body because HTTP Request Method was "GET" but only [POST PUT PATCH] are supported. stack_trace:
2022-06-12T14:01:57.086955040Z github.com/ory/x/decoderx.(*HTTP).validateRequest
2022-06-12T14:01:57.086984233Z 	/go/pkg/mod/github.com/ory/[email protected]/decoderx/http.go:228
2022-06-12T14:01:57.087011332Z github.com/ory/x/decoderx.(*HTTP).Decode
2022-06-12T14:01:57.087039059Z 	/go/pkg/mod/github.com/ory/[email protected]/decoderx/http.go:271
2022-06-12T14:01:57.087064830Z github.com/ory/kratos/selfservice/strategy/webauthn.(*Strategy).Login
2022-06-12T14:01:57.087091789Z 	/project/selfservice/strategy/webauthn/login.go:198
2022-06-12T14:01:57.087120424Z github.com/ory/kratos/selfservice/flow/login.(*Handler).submitFlow
2022-06-12T14:01:57.087137535Z 	/project/selfservice/flow/login/handler.go:592
2022-06-12T14:01:57.087154157Z github.com/ory/kratos/x.NoCacheHandle.func1
2022-06-12T14:01:57.087199764Z 	/project/x/nocache.go:18
2022-06-12T14:01:57.087238246Z github.com/ory/kratos/x.NoCacheHandle.func1
2022-06-12T14:01:57.087263599Z 	/project/x/nocache.go:18
2022-06-12T14:01:57.087307040Z github.com/julienschmidt/httprouter.(*Router).ServeHTTP
2022-06-12T14:01:57.087339237Z 	/go/pkg/mod/github.com/julienschmidt/[email protected]/router.go:387
2022-06-12T14:01:57.087371224Z github.com/ory/nosurf.(*CSRFHandler).handleSuccess
2022-06-12T14:01:57.087401395Z 	/go/pkg/mod/github.com/ory/[email protected]/handler.go:234
2022-06-12T14:01:57.087427586Z github.com/ory/nosurf.(*CSRFHandler).ServeHTTP
2022-06-12T14:01:57.087469910Z 	/go/pkg/mod/github.com/ory/[email protected]/handler.go:185
2022-06-12T14:01:57.087501897Z github.com/urfave/negroni.Wrap.func1
2022-06-12T14:01:57.087537097Z 	/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:46
2022-06-12T14:01:57.087663929Z github.com/urfave/negroni.HandlerFunc.ServeHTTP
2022-06-12T14:01:57.087752976Z 	/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:29
2022-06-12T14:01:57.087788735Z github.com/urfave/negroni.middleware.ServeHTTP
2022-06-12T14:01:57.087841465Z 	/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:38
2022-06-12T14:01:57.087902995Z github.com/ory/kratos/x.glob..func1
2022-06-12T14:01:57.087985618Z 	/project/x/clean_url.go:12
2022-06-12T14:01:57.088030246Z github.com/urfave/negroni.HandlerFunc.ServeHTTP
2022-06-12T14:01:57.088060557Z 	/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:29
2022-06-12T14:01:57.088085491Z github.com/urfave/negroni.middleware.ServeHTTP
2022-06-12T14:01:57.088124253Z 	/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:38
2022-06-12T14:01:57.088178100Z net/http.HandlerFunc.ServeHTTP
2022-06-12T14:01:57.088206456Z 	/usr/local/go/src/net/http/server.go:2047
2022-06-12T14:01:57.088228316Z github.com/prometheus/client_golang/prometheus/promhttp.InstrumentHandlerResponseSize.func1
2022-06-12T14:01:57.088259815Z 	/go/pkg/mod/github.com/prometheus/[email protected]/prometheus/promhttp/instrument_server.go:198
2022-06-12T14:01:57.088284957Z net/http.HandlerFunc.ServeHTTP
2022-06-12T14:01:57.088306189Z 	/usr/local/go/src/net/http/server.go:2047
2022-06-12T14:01:57.088330564Z github.com/prometheus/client_golang/prometheus/promhttp.InstrumentHandlerCounter.func1
2022-06-12T14:01:57.088366323Z 	/go/pkg/mod/github.com/prometheus/[email protected]/prometheus/promhttp/instrument_server.go:101
2022-06-12T14:01:57.088409205Z net/http.HandlerFunc.ServeHTTP
2022-06-12T14:01:57.088435675Z 	/usr/local/go/src/net/http/server.go:2047
2022-06-12T14:01:57.088461935Z github.com/prometheus/client_golang/prometheus/promhttp.InstrumentHandlerDuration.func1
2022-06-12T14:01:57.088476323Z 	/go/pkg/mod/github.com/prometheus/[email protected]/prometheus/promhttp/instrument_server.go:68
2022-06-12T14:01:57.088526748Z net/http.HandlerFunc.ServeHTTP
2022-06-12T14:01:57.088554684Z 	/usr/local/go/src/net/http/server.go:2047
2022-06-12T14:01:57.088573751Z github.com/prometheus/client_golang/prometheus/promhttp.InstrumentHandlerDuration.func2
2022-06-12T14:01:57.088602037Z 	/go/pkg/mod/github.com/prometheus/[email protected]/prometheus/promhttp/instrument_server.go:76
2022-06-12T14:01:57.088624526Z net/http.HandlerFunc.ServeHTTP
2022-06-12T14:01:57.088657072Z 	/usr/local/go/src/net/http/server.go:2047
2022-06-12T14:01:57.088681656Z github.com/prometheus/client_golang/prometheus/promhttp.InstrumentHandlerRequestSize.func1
2022-06-12T14:01:57.088710361Z 	/go/pkg/mod/github.com/prometheus/[email protected]/prometheus/promhttp/instrument_server.go:165
2022-06-12T14:01:57.088869389Z net/http.HandlerFunc.ServeHTTP
2022-06-12T14:01:57.088906964Z 	/usr/local/go/src/net/http/server.go:2047
2022-06-12T14:01:57.088930221Z github.com/ory/x/prometheusx.Metrics.instrumentHandlerStatusBucket.func1
2022-06-12T14:01:57.088957738Z 	/go/pkg/mod/github.com/ory/[email protected]/prometheusx/metrics.go:108
2022-06-12T14:01:57.088986513Z net/http.HandlerFunc.ServeHTTP
2022-06-12T14:01:57.089015986Z 	/usr/local/go/src/net/http/server.go:2047
2022-06-12T14:01:57.089041758Z github.com/ory/x/prometheusx.(*MetricsManager).ServeHTTP
2022-06-12T14:01:57.089064875Z 	/go/pkg/mod/github.com/ory/[email protected]/prometheusx/middleware.go:30
2022-06-12T14:01:57.089116627Z github.com/urfave/negroni.middleware.ServeHTTP
2022-06-12T14:01:57.089135275Z 	/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:38
2022-06-12T14:01:57.089159091Z github.com/ory/x/metricsx.(*Service).ServeHTTP
2022-06-12T14:01:57.089179834Z 	/go/pkg/mod/github.com/ory/[email protected]/metricsx/middleware.go:275
2022-06-12T14:01:57.089193313Z github.com/urfave/negroni.middleware.ServeHTTP
2022-06-12T14:01:57.089240316Z 	/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:38 status:Bad Request status_code:400] http_request=map[headers:map[accept-encoding:gzip, deflate connection:close] host:localhost:4433 method:GET path:/self-service/login query:flow=40013f4d-bd56-4769-8d58-30598972762c remote:172.18.0.1:60604 scheme:http] login_flow=&{40013f4d-bd56-4769-8d58-30598972762c 1ea7c4c7-0aea-4341-b96d-29685ee01608 browser 2022-06-12 14:11:55.119654028 +0000 UTC 2022-06-12 14:01:55.119654028 +0000 UTC [123 125] http://localhost:4433/self-service/login/browser?refresh=false   0xc000a61b30 2022-06-12 14:01:55.128828 +0000 UTC 2022-06-12 14:01:55.128828 +0000 UTC iA302rcrVp4ruSXCbhVHnd79ucSev0mrw0YOBoXeG2wElpkgrxDAB1UvyeZCXBH1H3yFWKDV+WxlpZ9lEM1xOA== false aal1} service_name=Ory Kratos service_version=v0.10.1
2022-06-12T14:01:57.100084711Z time=2022-06-12T14:01:57Z level=info msg=completed handling request func=github.com/ory/x/reqlog.(*Middleware).ServeHTTP file=/go/pkg/mod/github.com/ory/[email protected]/reqlog/middleware.go:139 http_request=map[headers:map[accept-encoding:gzip, deflate connection:close] host:localhost:4433 method:GET path:/self-service/login query:flow=40013f4d-bd56-4769-8d58-30598972762c remote:172.18.0.1:60604 scheme:http] http_response=map[headers:map[cache-control:private, no-cache, no-store, must-revalidate content-type:text/html; charset=utf-8 location:http://localhost:3000/login?flow=40013f4d-bd56-4769-8d58-30598972762c vary:Origin] size:101 status:303 text_status:See Other took:17.525062ms]

Add helper function and add it to the nextjs example

Right now it's not clear how to pass cookies using Axios for next-edge integration.

As a solution it would be best to have a helper function here

  const test = await fetch(`${process.env.ORY_SDK_URL!}/sessions/whoami`, {
    headers: {
      "Cookie": "ory_session_xxx=....; ory_csrf_xxx=....",
    }
  })

and add it to our nextJS example.

Support Next.js experimental runtime edge

To deploy Next.js 13 to a provider like Cloudflare you need to use the new experimental edge runtime "edge" but this doesn't work with the integration provided here due to the use of request.

error - Error: The edge runtime does not support Node.js 'util' module.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
    at <unknown> (webpack-internal:///(middleware)/./node_modules/next/dist/esm/server/web/adapter.js:189)
    at Object.get (webpack-internal:///(middleware)/./node_modules/next/dist/esm/server/web/adapter.js:189:19)
    at eval (webpack-internal:///(middleware)/./node_modules/tough-cookie/lib/memstore.js:55:18)
    at Object.(middleware)/./node_modules/tough-cookie/lib/memstore.js (evalmachine.<anonymous>:2021:1)
    at __webpack_require__ (evalmachine.<anonymous>:37:33)
    at fn (evalmachine.<anonymous>:281:21)
    at eval (webpack-internal:///(middleware)/./node_modules/tough-cookie/lib/cookie.js:37:26)
    at Object.(middleware)/./node_modules/tough-cookie/lib/cookie.js (evalmachine.<anonymous>:2010:1)
    at __webpack_require__ (evalmachine.<anonymous>:37:33)
    at fn (evalmachine.<anonymous>:281:21)
    at eval (webpack-internal:///(middleware)/./node_modules/request/lib/cookies.js:3:13)

Next.js basePath handling

I am running OpenFAAS based setup with multiple servers on different sub paths of same domain - mydomain.com/function/app1, mydomain.com/function/app2 etc.

Now i'm trying to set up a Next + Ory app at mydomain.com/function/nextapp

I am using Next.js 12 with @ory/client 1.1.4 and @ory/integrations 1.1.0

I have a basePath: '/function/nextapp' in my next.config.js because I need the ory login pages to live on mydomain.com/function/nextapp/api/.ory/ui/login instead of mydomain.com/api/.ory/ui/login

Unfortunately, no matter what I try, I always end up being redirected to mydomain.com/api/.ory/ui/login?flow=...

  • I tried manually going to the URL mydomain.com/function/nextapp/api/.ory/ui/login
  • I tried explicitly redirecting the user to the absolute url such as router.push('mydomain.com/function/nextapp/api/.ory/ui/login')
  • I tried explicitly overriding the edgeConfig.basePath to /function/nextapp/api/.ory/ when instantiating FrontendApi such as new FrontendApi(new Configuration({ basePath: 'mydomain.com/function/nextapp/api/.ory/ui/login' }))

Looks like the currently suggested Next + Ory setup ignores the next.config.js basePath and it is not possible to override it manually either.
Looks like the FrontendApi ignores edgeConfig and always redirects to the root relative basePath /api/.ory/ui/login

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.